diff --git a/source/mkdocs_deploy/abstract.py b/source/mkdocs_deploy/abstract.py index a43835b..6ff117e 100644 --- a/source/mkdocs_deploy/abstract.py +++ b/source/mkdocs_deploy/abstract.py @@ -1,6 +1,7 @@ import urllib.parse from abc import abstractmethod -from typing import Callable, IO, Iterable, Optional, Protocol, Union +from enum import Enum +from typing import Callable, IO, Iterable, Optional, Protocol from .versions import DeploymentAlias, DeploymentSpec @@ -13,6 +14,16 @@ class RedirectMechanismNotFound(Exception): pass +class _RootDirType(Enum): + ROOT_DIR = 0 + + +ROOT_DIR = _RootDirType.ROOT_DIR + + +Version = str | _RootDirType + + class Source(Protocol): """ Source is where a site is loaded from. @@ -79,7 +90,7 @@ def delete_version(self, version_id: str) -> None: """ @abstractmethod - def upload_file(self, version_id: str | None, filename: str, file_obj: IO[bytes]) -> None: + def upload_file(self, version_id: Version, filename: str, file_obj: IO[bytes]) -> None: """ Upload a file to the target @@ -90,7 +101,7 @@ def upload_file(self, version_id: str | None, filename: str, file_obj: IO[bytes] """ @abstractmethod - def download_file(self, version_id: str | None, filename: str) -> IO[bytes]: + def download_file(self, version_id: Version, filename: str) -> IO[bytes]: """ Open a file handle to read content of a file @@ -105,7 +116,7 @@ def download_file(self, version_id: str | None, filename: str) -> IO[bytes]: @abstractmethod - def delete_file(self, version_id: str | None, filename: str) -> None: + def delete_file(self, version_id: Version, filename: str) -> None: """ Delete a file, or mark it for deletion on close. :param version_id: The version to delete from @@ -133,7 +144,7 @@ def close(self, success: bool = False) -> None: """ @abstractmethod - def set_alias(self, alias_id: str | None, alias: Optional[DeploymentAlias]) -> None: + def set_alias(self, alias_id: Version, alias: Optional[DeploymentAlias]) -> None: """ Create or delete an alias. @@ -185,18 +196,18 @@ class RedirectMechanism(Protocol): """ @abstractmethod - def create_redirect(self, session: TargetSession, alias: Union[str, type(...)], version_id: str) -> None: + def create_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None: """ Create a redirect :param session: The TargetSession to apply changes to - :param alias: The new alias to create. If ``...`` is passed, then a redirect from the root is created. IE: "" + :param alias: The new alias to create. If ``None`` is passed, then a redirect from the root is created. IE: "" defines what the default version is. :param version_id: The version to redirect to. """ @abstractmethod - def delete_redirect(self, session: TargetSession, alias: Union[str, type(...)]) -> None: + def delete_redirect(self, session: TargetSession, alias:Version) -> None: """ Delete the named redirect. @@ -204,7 +215,7 @@ def delete_redirect(self, session: TargetSession, alias: Union[str, type(...)]) :param alias: The alias to delete. ``...`` is the default redirect. """ - def refresh_redirect(self, session: TargetSession, alias: Union[str, type(...)], version_id: str) -> None: + def refresh_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None: """ Called to ensure all redirects still work after a version has been altered. diff --git a/source/tests/conftest.py b/source/tests/conftest.py index 7a44795..7e9eaf4 100644 --- a/source/tests/conftest.py +++ b/source/tests/conftest.py @@ -1,9 +1,6 @@ -from pathlib import Path - import pytest from mkdocs_deploy import abstract -from mkdocs_deploy.plugins import local_filesystem @pytest.fixture(autouse=True) @@ -11,26 +8,3 @@ def _clean_plugins(monkeypatch: pytest.MonkeyPatch): """Ensure that all tests run with uninitialized plugins""" monkeypatch.setattr(abstract, "_SOURCES", {}) monkeypatch.setattr(abstract, "_TARGETS", {}) - -@pytest.fixture() -def mock_source_path(tmp_path: Path) -> Path: - base_path = tmp_path / "mock_source" - base_path.mkdir(exist_ok=False, parents=True) - return base_path - - -@pytest.fixture() -def mock_source(mock_source_path: Path) -> abstract.Source: - return local_filesystem.LocalFileTreeSource(mock_source_path) - - -@pytest.fixture() -def mock_target_path(tmp_path: Path) -> Path: - base_path = tmp_path / "mock_target" - base_path.mkdir(exist_ok=False, parents=True) - return base_path - - -@pytest.fixture() -def mock_target(mock_target_path: Path) -> abstract.Target: - return local_filesystem.LocalFileTreeTarget(str(mock_target_path)) diff --git a/source/tests/mock_plugin.py b/source/tests/mock_plugin.py new file mode 100644 index 0000000..b306846 --- /dev/null +++ b/source/tests/mock_plugin.py @@ -0,0 +1,111 @@ +import io +from copy import deepcopy +from io import BytesIO +from typing import IO, Iterable + +from mkdocs_deploy import abstract, versions +from mkdocs_deploy.abstract import TargetSession, Version +from mkdocs_deploy.versions import DeploymentAlias, DeploymentSpec + + +class BaseMockPlugin: + + files: dict[str, bytes] + + def __init__(self): + self.files = {} + + +class MockSource(abstract.Source): + + def __init__(self, files: dict[str, bytes] | None = None): + self.files = files.copy() if files is not None else {} + + def iter_files(self) -> Iterable[str]: + yield from self.files.keys() + + def open_file_for_read(self, filename: str) -> IO[bytes]: + return io.BytesIO(initial_bytes=self.files[filename]) + + +class MockRedirectMechanism(abstract.RedirectMechanism): + + def create_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None: + pass + + def delete_redirect(self, session: TargetSession, alias: Version) -> None: + pass + + +class MockTargetSession(abstract.TargetSession): + files: dict[tuple[Version, str], bytes] + internal_deployment_spec: abstract.DeploymentSpec + closed: bool = False + close_success: bool = False + aliases: dict[Version, versions.DeploymentAlias] + + def __init__(self): + self.files = {} + self.deleted_files = set() + self.internal_deployment_spec = abstract.DeploymentSpec() + self.aliases = {} + + def start_version(self, version_id: str, title: str) -> None: + self.internal_deployment_spec.versions[version_id] = versions.DeploymentVersion(title=title) + + def delete_version(self, version_id: str) -> None: + existing_files = [f for v, f in self.files.keys() if v == version_id] + del self.internal_deployment_spec.versions[version_id] + for file in existing_files: + del self.files[(version_id, file)] + + def upload_file(self, version_id: Version, filename: str, file_obj: IO[bytes]) -> None: + if version_id not in self.internal_deployment_spec.versions: + raise abstract.VersionNotFound(version_id) + self.files[(version_id, filename)] = file_obj.read() + + def download_file(self, version_id: Version, filename: str) -> IO[bytes]: + if version_id not in self.internal_deployment_spec.versions: + raise abstract.VersionNotFound(version_id) + return BytesIO(self.files[(version_id, filename)]) + + def delete_file(self, version_id: Version, filename: str) -> None: + if version_id not in self.internal_deployment_spec.versions: + raise abstract.VersionNotFound(version_id) + del self.files[(version_id, filename)] + + def iter_files(self, version_id: str) -> Iterable[str]: + for version, file in self.files: + if version_id == version: + yield file + + def close(self, success: bool = False) -> None: + self.closed = True + self.close_success = success + + def set_alias(self, alias_id: Version, alias: DeploymentAlias | None) -> None: + if alias is None: + del self.aliases[alias_id] + return + self.aliases[alias_id] = deepcopy(alias) + + @property + def available_redirect_mechanisms(self) -> dict[str, abstract.RedirectMechanism]: + return {"mock": MockRedirectMechanism()} + + @property + def deployment_spec(self) -> DeploymentSpec: + return deepcopy(self.internal_deployment_spec) + + +class MockTarget(abstract.Target): + + files: dict[tuple[str, str], bytes] + internal_deployment_spec: versions.DeploymentSpec + + def __init__(self): + self.files = {} + self.internal_deployment_spec = versions.DeploymentSpec() + + def start_session(self) -> MockTargetSession: + return MockTargetSession() diff --git a/source/tests/mock_wrapper.py b/source/tests/mock_wrapper.py index 733f172..3744c27 100644 --- a/source/tests/mock_wrapper.py +++ b/source/tests/mock_wrapper.py @@ -1,5 +1,4 @@ from typing import Any, NamedTuple, TypeVar -from copy import deepcopy _T = TypeVar("_T") diff --git a/source/tests/test_modules/test_actions.py b/source/tests/test_modules/test_actions.py index 3c7d9bd..d672885 100644 --- a/source/tests/test_modules/test_actions.py +++ b/source/tests/test_modules/test_actions.py @@ -1,46 +1,29 @@ import uuid -from pathlib import Path import pytest -from mkdocs_deploy import abstract, actions, versions +from mkdocs_deploy import actions, versions +from ..mock_plugin import MockSource, MockTargetSession from ..mock_wrapper import mock_wrapper - -@pytest.fixture() -def mock_source_files(mock_source: abstract.Source, mock_source_path: Path) -> dict[str, bytes]: - (mock_source_path / "index.html").write_text(str(uuid.uuid4())) - (mock_source_path / "subdir").mkdir(exist_ok=False) - (mock_source_path / "subdir" / "foo.txt").write_text(str(uuid.uuid4())) - results = {} - for file in mock_source.iter_files(): - with mock_source.open_file_for_read(file) as file_handle: - results[file] = file_handle.read() - return results +MOCK_SOURCE_FILES = { + "index.html": str(uuid.uuid4()).encode(), + "subdir/foo.txt": str(uuid.uuid4()).encode(), +} @pytest.mark.parametrize("title", ["Version 1.1", None], ids=["Explicit Title", "Implicit Title"]) -def test_upload( - mock_source: abstract.Source, mock_target: abstract.Target, mock_source_files: dict[str, bytes], title: str | None -): +def test_upload(title: str | None): VERSION = "1.1" - session, session_method_calls = mock_wrapper(mock_target.start_session()) + source = MockSource(MOCK_SOURCE_FILES) + session, session_method_calls = mock_wrapper(MockTargetSession()) + try: - actions.upload( - source=mock_source, - target=session, - version_id=VERSION, - title=title, - ) + actions.upload(source=source, target=session, version_id=VERSION, title=title) assert session_method_calls[0].name == "start_version" - uploaded_files = set(session.iter_files(VERSION)) - assert uploaded_files == set(mock_source_files.keys()) - for file in mock_source_files.keys(): - with session.download_file(VERSION, file) as file_handle: - assert file_handle.read() == mock_source_files[file] - + assert session.files == {(VERSION, file): content for file, content in source.files.items()} assert session.deployment_spec.versions == { VERSION: versions.DeploymentVersion(title=title or VERSION), @@ -52,64 +35,28 @@ def test_upload( session.close(success=True) -def test_upload_implicit_title_does_not_override_existing_one( - mock_source: abstract.Source, mock_target: abstract.Target, mock_source_files: dict[str, bytes] -): +def test_upload_implicit_title_does_not_override_existing_one(): VERSION = "1.1" - session = mock_target.start_session() - try: - session._deployment_spec.versions[VERSION] = versions.DeploymentVersion(title="foo bar") # type: ignore - actions.upload( - source=mock_source, - target=session, - version_id=VERSION, - title=None, - ) - - uploaded_files = set(session.iter_files(VERSION)) - assert uploaded_files == set(mock_source_files.keys()) - for file in mock_source_files.keys(): - with session.download_file(VERSION, file) as file_handle: - assert file_handle.read() == mock_source_files[file] + source = MockSource(MOCK_SOURCE_FILES) + session, session_method_calls = mock_wrapper(MockTargetSession()) + session.start_version(VERSION, "foo bar") + actions.upload(source=source, target=session, version_id=VERSION, title=None) - assert session.deployment_spec.versions == { - VERSION: versions.DeploymentVersion(title="foo bar" or VERSION), - } - except: - session.close(success=False) - raise - else: - session.close(success=True) + assert session.deployment_spec.versions == { + VERSION: versions.DeploymentVersion(title="foo bar"), + } -def test_upload_explicit_title_overrides_existing_one( - mock_source: abstract.Source, mock_target: abstract.Target, mock_source_files: dict[str, bytes] -): +def test_upload_explicit_title_overrides_existing_one(): VERSION = "1.1" VERSION_TITLE = "Version 1.1" - session = mock_target.start_session() - try: - session._deployment_spec.versions[VERSION] = versions.DeploymentVersion(title="foo bar") # type: ignore - actions.upload( - source=mock_source, - target=session, - version_id=VERSION, - title=VERSION_TITLE, - ) + source = MockSource(MOCK_SOURCE_FILES) + session, session_method_calls = mock_wrapper(MockTargetSession()) + session.start_version(VERSION, "foo bar") - uploaded_files = set(session.iter_files(VERSION)) - assert uploaded_files == set(mock_source_files.keys()) - for file in mock_source_files.keys(): - with session.download_file(VERSION, file) as file_handle: - assert file_handle.read() == mock_source_files[file] + actions.upload(source=source, target=session, version_id=VERSION, title=VERSION_TITLE) - - assert session.deployment_spec.versions == { - VERSION: versions.DeploymentVersion(title=VERSION_TITLE or VERSION), - } - except: - session.close(success=False) - raise - else: - session.close(success=True) \ No newline at end of file + assert session.deployment_spec.versions == { + VERSION: versions.DeploymentVersion(title=VERSION_TITLE), + } \ No newline at end of file