diff --git a/scenarios/requires-does-not-exist.json b/scenarios/requires-does-not-exist.json new file mode 100644 index 00000000..85ec9e45 --- /dev/null +++ b/scenarios/requires-does-not-exist.json @@ -0,0 +1,17 @@ +{ + "name": "requires-does-not-exist", + "description": "Package `a` requires any version of package `b` which does not exist", + "root": "a", + "packages": { + "a": { + "versions": { + "1.0.0": { + "requires_python": ">=3.7", + "requires": [ + "b" + ] + } + } + } + } +} diff --git a/src/packse/build.py b/src/packse/build.py index 323469d1..3476afde 100644 --- a/src/packse/build.py +++ b/src/packse/build.py @@ -5,6 +5,7 @@ import shutil import subprocess from pathlib import Path +from typing import Generator from packse.error import ( BuildError, @@ -27,7 +28,7 @@ def build(targets: list[Path], rm_destination: bool): try: load_scenario(target) except Exception as exc: - raise InvalidScenario(target) from exc + raise InvalidScenario(target, reason=str(exc)) from exc # Then build each one for target in targets: @@ -118,23 +119,27 @@ def build_scenario_package( ) try: - dists = build_package_distributions(package_destination) + for dist in build_package_distributions(package_destination): + shared_path = dist_destination / dist.name + logger.info( + "Linked distribution to %s", shared_path.relative_to(work_dir) + ) + shared_path.hardlink_to(dist) except subprocess.CalledProcessError as exc: raise BuildError( f"Building {package_destination.relative_to(work_dir)} with hatch failed", exc.output.decode(), ) - for dist in dists: - shared_path = dist_destination / dist.name - logger.info("Linked distribution to %s", shared_path.relative_to(work_dir)) - shared_path.hardlink_to(dist) - -def build_package_distributions(target: Path) -> tuple[Path]: +def build_package_distributions(target: Path) -> Generator[Path, None, None]: + """ + Build package distributions, yield each built distribution path, then delete the distribution folder. + """ subprocess.check_output( ["hatch", "build"], cwd=target, stderr=subprocess.STDOUT, ) - return tuple((target / "dist").iterdir()) + yield from sorted((target / "dist").iterdir()) + shutil.rmtree(target / "dist") diff --git a/src/packse/error.py b/src/packse/error.py index 0240747f..5794ca11 100644 --- a/src/packse/error.py +++ b/src/packse/error.py @@ -17,8 +17,8 @@ def __init__(self, destination: Path) -> None: class InvalidScenario(UserError): - def __init__(self, path: Path) -> None: - message = f"File at '{path}' is not a valid scenario" + def __init__(self, path: Path, reason: str) -> None: + message = f"File at '{path}' is not a valid scenario: {reason}" super().__init__(message) diff --git a/src/packse/scenario.py b/src/packse/scenario.py index 0567fc6a..ac7cd3c8 100644 --- a/src/packse/scenario.py +++ b/src/packse/scenario.py @@ -56,6 +56,11 @@ class Scenario(msgspec.Struct): The template to use for scenario packages. """ + description: str | None = None + """ + The description of the scenario + """ + def hash(self) -> str: """ Return a hash of the scenario contents diff --git a/src/packse/view.py b/src/packse/view.py index f4e51fc1..ad29eeec 100644 --- a/src/packse/view.py +++ b/src/packse/view.py @@ -21,7 +21,7 @@ def view(targets: list[Path]): try: load_scenario(target) except Exception as exc: - raise InvalidScenario(target) from exc + raise InvalidScenario(target, reason=str(exc)) from exc # Then view each one for target in targets: diff --git a/tests/__snapshots__/test_build.ambr b/tests/__snapshots__/test_build.ambr index 603b9060..ed22ccbe 100644 --- a/tests/__snapshots__/test_build.ambr +++ b/tests/__snapshots__/test_build.ambr @@ -3,8 +3,6 @@ dict({ 'exit_code': 0, 'filesystem': dict({ - 'build/example-9e723676/example-9e723676-a-1.0.0/dist/example_9e723676_a-1.0.0-py3-none-any.whl': 'md5:38bdcf701bb74c615cda5cd8dd4d4ce3', - 'build/example-9e723676/example-9e723676-a-1.0.0/dist/example_9e723676_a-1.0.0.tar.gz': 'md5:cf5d79fbc9777d41a69801f79edff4de', 'build/example-9e723676/example-9e723676-a-1.0.0/pyproject.toml': ''' [build-system] requires = ["hatchling"] @@ -24,8 +22,6 @@ __version__ = "1.0.0" ''', - 'build/example-9e723676/example-9e723676-b-1.0.0/dist/example_9e723676_b-1.0.0-py3-none-any.whl': 'md5:8af703510c999651652071adbf469c20', - 'build/example-9e723676/example-9e723676-b-1.0.0/dist/example_9e723676_b-1.0.0.tar.gz': 'md5:9bcf4c09b49d56fc6cf3ecdfeeac4ba0', 'build/example-9e723676/example-9e723676-b-1.0.0/pyproject.toml': ''' [build-system] requires = ["hatchling"] @@ -54,17 +50,11 @@ ├── build │ └── example-9e723676 │ ├── example-9e723676-a-1.0.0 - │ │ ├── dist - │ │ │ ├── example_9e723676_a-1.0.0-py3-none-any.whl - │ │ │ └── example_9e723676_a-1.0.0.tar.gz │ │ ├── pyproject.toml │ │ └── src │ │ └── example_9e723676_a │ │ └── __init__.py │ └── example-9e723676-b-1.0.0 - │ ├── dist - │ │ ├── example_9e723676_b-1.0.0-py3-none-any.whl - │ │ └── example_9e723676_b-1.0.0.tar.gz │ ├── pyproject.toml │ └── src │ └── example_9e723676_b @@ -76,7 +66,7 @@ ├── example_9e723676_b-1.0.0-py3-none-any.whl └── example_9e723676_b-1.0.0.tar.gz - 12 directories, 12 files + 10 directories, 8 files ''', }), @@ -96,8 +86,8 @@ INFO:packse.template:Creating example-9e723676-b-1.0.0/src/example_9e723676_b INFO:packse.template:Creating example-9e723676-b-1.0.0/src/example_9e723676_b/__init__.py INFO:packse.build:Building build/example-9e723676/example-9e723676-b-1.0.0 with hatch - INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0.tar.gz INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0-py3-none-any.whl + INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0.tar.gz ''', 'stdout': ''' @@ -110,8 +100,6 @@ dict({ 'exit_code': 0, 'filesystem': dict({ - 'build/example-9e723676/example-9e723676-a-1.0.0/dist/example_9e723676_a-1.0.0-py3-none-any.whl': 'md5:38bdcf701bb74c615cda5cd8dd4d4ce3', - 'build/example-9e723676/example-9e723676-a-1.0.0/dist/example_9e723676_a-1.0.0.tar.gz': 'md5:cf5d79fbc9777d41a69801f79edff4de', 'build/example-9e723676/example-9e723676-a-1.0.0/pyproject.toml': ''' [build-system] requires = ["hatchling"] @@ -131,8 +119,6 @@ __version__ = "1.0.0" ''', - 'build/example-9e723676/example-9e723676-b-1.0.0/dist/example_9e723676_b-1.0.0-py3-none-any.whl': 'md5:8af703510c999651652071adbf469c20', - 'build/example-9e723676/example-9e723676-b-1.0.0/dist/example_9e723676_b-1.0.0.tar.gz': 'md5:9bcf4c09b49d56fc6cf3ecdfeeac4ba0', 'build/example-9e723676/example-9e723676-b-1.0.0/pyproject.toml': ''' [build-system] requires = ["hatchling"] @@ -161,17 +147,11 @@ ├── build │ └── example-9e723676 │ ├── example-9e723676-a-1.0.0 - │ │ ├── dist - │ │ │ ├── example_9e723676_a-1.0.0-py3-none-any.whl - │ │ │ └── example_9e723676_a-1.0.0.tar.gz │ │ ├── pyproject.toml │ │ └── src │ │ └── example_9e723676_a │ │ └── __init__.py │ └── example-9e723676-b-1.0.0 - │ ├── dist - │ │ ├── example_9e723676_b-1.0.0-py3-none-any.whl - │ │ └── example_9e723676_b-1.0.0.tar.gz │ ├── pyproject.toml │ └── src │ └── example_9e723676_b @@ -183,7 +163,7 @@ ├── example_9e723676_b-1.0.0-py3-none-any.whl └── example_9e723676_b-1.0.0.tar.gz - 12 directories, 12 files + 10 directories, 8 files ''', }), @@ -203,8 +183,8 @@ INFO:packse.template:Creating example-9e723676-b-1.0.0/src/example_9e723676_b INFO:packse.template:Creating example-9e723676-b-1.0.0/src/example_9e723676_b/__init__.py INFO:packse.build:Building build/example-9e723676/example-9e723676-b-1.0.0 with hatch - INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0.tar.gz INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0-py3-none-any.whl + INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0.tar.gz ''', 'stdout': ''' @@ -232,8 +212,8 @@ INFO:packse.template:Creating example-9e723676-b-1.0.0/src/example_9e723676_b INFO:packse.template:Creating example-9e723676-b-1.0.0/src/example_9e723676_b/__init__.py INFO:packse.build:Building build/example-9e723676/example-9e723676-b-1.0.0 with hatch - INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0.tar.gz INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0-py3-none-any.whl + INFO:packse.build:Linked distribution to dist/example-9e723676/example_9e723676_b-1.0.0.tar.gz ''', 'stdout': ''' @@ -246,7 +226,7 @@ dict({ 'exit_code': 1, 'stderr': ''' - File at '$PWD/test.json' is not a valid scenario. + File at '$PWD/test.json' is not a valid scenario: Input data was truncated. ''', 'stdout': '', diff --git a/tests/__snapshots__/test_scenarios.ambr b/tests/__snapshots__/test_scenarios.ambr new file mode 100644 index 00000000..6ff93f5e --- /dev/null +++ b/tests/__snapshots__/test_scenarios.ambr @@ -0,0 +1,83 @@ +# serializer version: 1 +# name: test_build_all_scenarios[requires-does-not-exist] + dict({ + 'exit_code': 0, + 'filesystem': dict({ + 'build/requires-does-not-exist-1888d2a8/requires-does-not-exist-1888d2a8-a-1.0.0/pyproject.toml': ''' + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.hatch.build] + sources = ["src"] + + [project] + name = "requires-does-not-exist-1888d2a8-a" + version = "1.0.0" + dependencies = ["requires-does-not-exist-1888d2a8-b"] + requires-python = ">=3.7" + + ''', + 'build/requires-does-not-exist-1888d2a8/requires-does-not-exist-1888d2a8-a-1.0.0/src/requires_does_not_exist_1888d2a8_a/__init__.py': ''' + __version__ = "1.0.0" + + ''', + 'dist/requires-does-not-exist-1888d2a8/requires_does_not_exist_1888d2a8_a-1.0.0-py3-none-any.whl': 'md5:0ede0ef9fce35b3b22369a4091b6d73d', + 'dist/requires-does-not-exist-1888d2a8/requires_does_not_exist_1888d2a8_a-1.0.0.tar.gz': 'md5:77825e5070d89109cb92e004c7adf79b', + 'tree': ''' + test_build_all_scenarios_requi0 + ├── build + │ └── requires-does-not-exist-1888d2a8 + │ └── requires-does-not-exist-1888d2a8-a-1.0.0 + │ ├── pyproject.toml + │ └── src + │ └── requires_does_not_exist_1888d2a8_a + │ └── __init__.py + └── dist + └── requires-does-not-exist-1888d2a8 + ├── requires_does_not_exist_1888d2a8_a-1.0.0-py3-none-any.whl + └── requires_does_not_exist_1888d2a8_a-1.0.0.tar.gz + + 7 directories, 4 files + + ''', + }), + 'stderr': ''' + INFO:root:Building 'requires-does-not-exist-1888d2a8' in directory 'build/requires-does-not-exist-1888d2a8' + INFO:packse.template:Creating requires-does-not-exist-1888d2a8-a-1.0.0 + INFO:packse.template:Creating requires-does-not-exist-1888d2a8-a-1.0.0/pyproject.toml + INFO:packse.template:Creating requires-does-not-exist-1888d2a8-a-1.0.0/src + INFO:packse.template:Creating requires-does-not-exist-1888d2a8-a-1.0.0/src/requires_does_not_exist_1888d2a8_a + INFO:packse.template:Creating requires-does-not-exist-1888d2a8-a-1.0.0/src/requires_does_not_exist_1888d2a8_a/__init__.py + INFO:packse.build:Building build/requires-does-not-exist-1888d2a8/requires-does-not-exist-1888d2a8-a-1.0.0 with hatch + INFO:packse.build:Linked distribution to dist/requires-does-not-exist-1888d2a8/requires_does_not_exist_1888d2a8_a-1.0.0-py3-none-any.whl + INFO:packse.build:Linked distribution to dist/requires-does-not-exist-1888d2a8/requires_does_not_exist_1888d2a8_a-1.0.0.tar.gz + + ''', + 'stdout': ''' + requires-does-not-exist-1888d2a8-a + + ''', + }) +# --- +# name: test_view_all_scenarios[requires-does-not-exist] + dict({ + 'exit_code': 0, + 'filesystem': dict({ + 'tree': ''' + test_view_all_scenarios_requir0 + + 0 directories + + ''', + }), + 'stderr': '', + 'stdout': ''' + requires-does-not-exist-1888d2a8 + └── a-1.0.0 + └── requires b + + + ''', + }) +# --- diff --git a/tests/__snapshots__/test_view.ambr b/tests/__snapshots__/test_view.ambr index b229588f..a5bb3699 100644 --- a/tests/__snapshots__/test_view.ambr +++ b/tests/__snapshots__/test_view.ambr @@ -18,7 +18,7 @@ dict({ 'exit_code': 1, 'stderr': ''' - File at '$PWD/test.json' is not a valid scenario. + File at '$PWD/test.json' is not a valid scenario: Input data was truncated. ''', 'stdout': '', diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py new file mode 100644 index 00000000..c5beb185 --- /dev/null +++ b/tests/test_scenarios.py @@ -0,0 +1,36 @@ +""" +Tests all included scenarios +""" + +import pytest +from packse import __development_base_path__ + +from .common import snapshot_command + +EXCLUDE = frozenset(("example.json",)) +ALL_SCENARIOS = tuple( + sorted( + path + for path in (__development_base_path__ / "scenarios").iterdir() + if path.is_file() and path.name.endswith(".json") and path.name not in EXCLUDE + ) +) +ALL_SCENARIO_IDS = tuple(path.name.removesuffix(".json") for path in ALL_SCENARIOS) + + +@pytest.mark.parametrize( + "target", + ALL_SCENARIOS, + ids=ALL_SCENARIO_IDS, +) +@pytest.mark.usefixtures("tmpcwd") +def test_build_all_scenarios(snapshot, target): + assert ( + snapshot_command(["build", str(target)], snapshot_filesystem=True) == snapshot + ) + + +@pytest.mark.parametrize("target", ALL_SCENARIOS, ids=ALL_SCENARIO_IDS) +@pytest.mark.usefixtures("tmpcwd") +def test_view_all_scenarios(snapshot, target): + assert snapshot_command(["view", str(target)], snapshot_filesystem=True) == snapshot