diff --git a/poetry.lock b/poetry.lock index 2588298f006..36bd49a456a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. [[package]] name = "attrs" @@ -731,14 +731,14 @@ files = [ [[package]] name = "installer" -version = "0.6.0" +version = "0.7.0" description = "A library for installing Python wheels." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "installer-0.6.0-py3-none-any.whl", hash = "sha256:ae7c62d1d6158b5c096419102ad0d01fdccebf857e784cee57f94165635fe038"}, - {file = "installer-0.6.0.tar.gz", hash = "sha256:f3bd36cd261b440a88a1190b1becca0578fee90b4b62decc796932fdd5ae8839"}, + {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, + {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, ] [[package]] @@ -1149,14 +1149,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "poetry-core" -version = "1.5.1" +version = "1.5.2" description = "Poetry PEP 517 Build Backend" category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "poetry_core-1.5.1-py3-none-any.whl", hash = "sha256:b1900dea81eb18feb7323d404e5f10430205541a4a683a912893f9d2b5807797"}, - {file = "poetry_core-1.5.1.tar.gz", hash = "sha256:41887261358863f25831fa0ad1fe7e451fc32d1c81fcf7710ba5174cc0047c6d"}, + {file = "poetry_core-1.5.2-py3-none-any.whl", hash = "sha256:832d40a1ea5fd10c0f648d0575cadddc8b79f06f91d83a1f1a73a7e1dfacfbd7"}, + {file = "poetry_core-1.5.2.tar.gz", hash = "sha256:c6556c3b1ec5b8668e6ef5a4494726bc41d31907339425e194e78a6178436c14"}, ] [package.dependencies] @@ -1983,4 +1983,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "d00f73e992cf3ea9c61769fe7841ce475d119422476025bab8415cd4f278ad26" +content-hash = "fb909b5c273da18b6715b134312d9a97edfa8dbfc2c7807fde3ace3d179c21ff" diff --git a/pyproject.toml b/pyproject.toml index 3e3fdb7c3e4..d01abd38610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ generate-setup-file = false [tool.poetry.dependencies] python = "^3.7" -poetry-core = "1.5.1" +poetry-core = "1.5.2" poetry-plugin-export = "^1.3.0" "backports.cached-property" = { version = "^1.0.2", python = "<3.8" } build = "^0.10.0" @@ -58,7 +58,7 @@ dulwich = "^0.21.2" filelock = "^3.8.0" html5lib = "^1.0" importlib-metadata = { version = ">=4.4", python = "<3.10" } -installer = "^0.6.0" +installer = "^0.7.0" jsonschema = "^4.10.0" keyring = "^23.9.0" lockfile = "^0.12.2" diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py index 6eda9699870..49b4631fbf4 100644 --- a/src/poetry/config/config.py +++ b/src/poetry/config/config.py @@ -210,7 +210,11 @@ def _get_environment_repositories() -> dict[str, dict[str, str]]: @property def repository_cache_directory(self) -> Path: - return Path(self.get("cache-dir")) / "cache" / "repositories" + return Path(self.get("cache-dir")).expanduser() / "cache" / "repositories" + + @property + def artifacts_cache_directory(self) -> Path: + return Path(self.get("cache-dir")).expanduser() / "artifacts" @property def virtualenvs_path(self) -> Path: diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py index 6eb1ae3f21b..5f57ba3e27b 100644 --- a/src/poetry/installation/chef.py +++ b/src/poetry/installation/chef.py @@ -1,7 +1,5 @@ from __future__ import annotations -import hashlib -import json import tarfile import tempfile import zipfile @@ -19,18 +17,14 @@ from poetry.core.utils.helpers import temporary_directory from pyproject_hooks import quiet_subprocess_runner # type: ignore[import] -from poetry.installation.chooser import InvalidWheelName -from poetry.installation.chooser import Wheel from poetry.utils.env import ephemeral_environment if TYPE_CHECKING: from contextlib import AbstractContextManager - from poetry.core.packages.utils.link import Link - - from poetry.config.config import Config from poetry.repositories import RepositoryPool + from poetry.utils.cache import ArtifactCache from poetry.utils.env import Env @@ -86,12 +80,12 @@ def install(self, requirements: Collection[str]) -> None: class Chef: - def __init__(self, config: Config, env: Env, pool: RepositoryPool) -> None: + def __init__( + self, artifact_cache: ArtifactCache, env: Env, pool: RepositoryPool + ) -> None: self._env = env self._pool = pool - self._cache_dir = ( - Path(config.get("cache-dir")).expanduser().joinpath("artifacts") - ) + self._artifact_cache = artifact_cache def prepare( self, archive: Path, output_dir: Path | None = None, *, editable: bool = False @@ -181,7 +175,9 @@ def _prepare_sdist(self, archive: Path, destination: Path | None = None) -> Path sdist_dir = archive_dir if destination is None: - destination = self.get_cache_directory_for_link(Link(archive.as_uri())) + destination = self._artifact_cache.get_cache_directory_for_link( + Link(archive.as_uri()) + ) destination.mkdir(parents=True, exist_ok=True) @@ -196,72 +192,3 @@ def _should_prepare(self, archive: Path) -> bool: @classmethod def _is_wheel(cls, archive: Path) -> bool: return archive.suffix == ".whl" - - def get_cached_archive_for_link(self, link: Link, *, strict: bool) -> Path | None: - archives = self.get_cached_archives_for_link(link) - if not archives: - return None - - candidates: list[tuple[float | None, Path]] = [] - for archive in archives: - if strict: - # in strict mode return the original cached archive instead of the - # prioritized archive type. - if link.filename == archive.name: - return archive - continue - if archive.suffix != ".whl": - candidates.append((float("inf"), archive)) - continue - - try: - wheel = Wheel(archive.name) - except InvalidWheelName: - continue - - if not wheel.is_supported_by_environment(self._env): - continue - - candidates.append( - (wheel.get_minimum_supported_index(self._env.supported_tags), archive), - ) - - if not candidates: - return None - - return min(candidates)[1] - - def get_cached_archives_for_link(self, link: Link) -> list[Path]: - cache_dir = self.get_cache_directory_for_link(link) - - archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"] - paths = [] - for archive_type in archive_types: - for archive in cache_dir.glob(f"*.{archive_type}"): - paths.append(Path(archive)) - - return paths - - def get_cache_directory_for_link(self, link: Link) -> Path: - key_parts = {"url": link.url_without_fragment} - - if link.hash_name is not None and link.hash is not None: - key_parts[link.hash_name] = link.hash - - if link.subdirectory_fragment: - key_parts["subdirectory"] = link.subdirectory_fragment - - key_parts["interpreter_name"] = self._env.marker_env["interpreter_name"] - key_parts["interpreter_version"] = "".join( - self._env.marker_env["interpreter_version"].split(".")[:2] - ) - - key = hashlib.sha256( - json.dumps( - key_parts, sort_keys=True, separators=(",", ":"), ensure_ascii=True - ).encode("ascii") - ).hexdigest() - - split_key = [key[:2], key[2:4], key[4:6], key[6:]] - - return self._cache_dir.joinpath(*split_key) diff --git a/src/poetry/installation/chooser.py b/src/poetry/installation/chooser.py index 821c09b38c8..b484504ed9a 100644 --- a/src/poetry/installation/chooser.py +++ b/src/poetry/installation/chooser.py @@ -6,11 +6,9 @@ from typing import TYPE_CHECKING from typing import Any -from packaging.tags import Tag - from poetry.config.config import Config from poetry.config.config import PackageFilterPolicy -from poetry.utils.patterns import wheel_file_re +from poetry.utils.wheel import Wheel if TYPE_CHECKING: @@ -25,37 +23,6 @@ logger = logging.getLogger(__name__) -class InvalidWheelName(Exception): - pass - - -class Wheel: - def __init__(self, filename: str) -> None: - wheel_info = wheel_file_re.match(filename) - if not wheel_info: - raise InvalidWheelName(f"{filename} is not a valid wheel filename.") - - self.filename = filename - self.name = wheel_info.group("name").replace("_", "-") - self.version = wheel_info.group("ver").replace("_", "-") - self.build_tag = wheel_info.group("build") - self.pyversions = wheel_info.group("pyver").split(".") - self.abis = wheel_info.group("abi").split(".") - self.plats = wheel_info.group("plat").split(".") - - self.tags = { - Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats - } - - def get_minimum_supported_index(self, tags: list[Tag]) -> int | None: - indexes = [tags.index(t) for t in self.tags if t in tags] - - return min(indexes) if indexes else None - - def is_supported_by_environment(self, env: Env) -> bool: - return bool(set(env.supported_tags).intersection(self.tags)) - - class Chooser: """ A Chooser chooses an appropriate release archive for packages. diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index 70e61a2cea6..ebbba0a4d6c 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -24,8 +24,10 @@ from poetry.installation.operations import Uninstall from poetry.installation.operations import Update from poetry.installation.wheel_installer import WheelInstaller +from poetry.puzzle.exceptions import SolverProblemError from poetry.utils._compat import decode from poetry.utils.authenticator import Authenticator +from poetry.utils.cache import ArtifactCache from poetry.utils.env import EnvCommandError from poetry.utils.helpers import atomic_open from poetry.utils.helpers import get_file_hash @@ -76,10 +78,11 @@ def __init__( else: self._max_workers = 1 + self._artifact_cache = ArtifactCache(cache_dir=config.artifacts_cache_directory) self._authenticator = Authenticator( config, self._io, disable_cache=disable_cache, pool_size=self._max_workers ) - self._chef = Chef(config, self._env, pool) + self._chef = Chef(self._artifact_cache, self._env, pool) self._chooser = Chooser(pool, self._env, config) self._executor = ThreadPoolExecutor(max_workers=self._max_workers) @@ -301,7 +304,14 @@ def _execute_operation(self, operation: Operation) -> None: trace.render(io) if isinstance(e, ChefBuildError): pkg = operation.package - requirement = pkg.to_dependency().to_pep_508() + pip_command = "pip wheel --use-pep517" + if pkg.develop: + requirement = pkg.source_url + pip_command += " --editable" + else: + requirement = ( + pkg.to_dependency().to_pep_508().split(";")[0].strip() + ) io.write_line("") io.write_line( "" @@ -309,9 +319,18 @@ def _execute_operation(self, operation: Operation) -> None: " and is likely not a problem with poetry" f" but with {pkg.pretty_name} ({pkg.full_pretty_version})" " not supporting PEP 517 builds. You can verify this by" - f" running 'pip wheel --use-pep517 \"{requirement}\"'." + f" running '{pip_command} \"{requirement}\"'." "" ) + elif isinstance(e, SolverProblemError): + pkg = operation.package + io.write_line("") + io.write_line( + "" + "Cannot resolve build-system.requires" + f" for {pkg.pretty_name}." + "" + ) io.write_line("") finally: with self._lock: @@ -692,15 +711,19 @@ def _download(self, operation: Install | Update) -> Path: def _download_link(self, operation: Install | Update, link: Link) -> Path: package = operation.package - output_dir = self._chef.get_cache_directory_for_link(link) + output_dir = self._artifact_cache.get_cache_directory_for_link(link) # Try to get cached original package for the link provided - original_archive = self._chef.get_cached_archive_for_link(link, strict=True) + original_archive = self._artifact_cache.get_cached_archive_for_link( + link, strict=True + ) if original_archive is None: # No cached original distributions was found, so we download and prepare it try: original_archive = self._download_archive(operation, link) except BaseException: - cache_directory = self._chef.get_cache_directory_for_link(link) + cache_directory = self._artifact_cache.get_cache_directory_for_link( + link + ) cached_file = cache_directory.joinpath(link.filename) # We can't use unlink(missing_ok=True) because it's not available # prior to Python 3.8 @@ -711,7 +734,11 @@ def _download_link(self, operation: Install | Update, link: Link) -> Path: # Get potential higher prioritized cached archive, otherwise it will fall back # to the original archive. - archive = self._chef.get_cached_archive_for_link(link, strict=False) + archive = self._artifact_cache.get_cached_archive_for_link( + link, + strict=False, + env=self._env, + ) # 'archive' can at this point never be None. Since we previously downloaded # an archive, we now should have something cached that we can use here assert archive is not None @@ -775,7 +802,9 @@ def _download_archive(self, operation: Install | Update, link: Link) -> Path: progress.start() done = 0 - archive = self._chef.get_cache_directory_for_link(link) / link.filename + archive = ( + self._artifact_cache.get_cache_directory_for_link(link) / link.filename + ) archive.parent.mkdir(parents=True, exist_ok=True) with atomic_open(archive) as f: for chunk in response.iter_content(chunk_size=4096): diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py index c8df26960f8..ab2e0a82f3e 100644 --- a/src/poetry/installation/wheel_installer.py +++ b/src/poetry/installation/wheel_installer.py @@ -94,10 +94,11 @@ def __init__(self, env: Env) -> None: ) def enable_bytecode_compilation(self, enable: bool = True) -> None: - self._destination.bytecode_optimization_levels = (1,) if enable else () + self._destination.bytecode_optimization_levels = (-1,) if enable else () def install(self, wheel: Path) -> None: - with WheelFile.open(Path(wheel.as_posix())) as source: + with WheelFile.open(wheel) as source: + source.validate_record() install( source=source, destination=self._destination.for_source(source), diff --git a/src/poetry/utils/cache.py b/src/poetry/utils/cache.py index ba88a077055..99bd5b40cee 100644 --- a/src/poetry/utils/cache.py +++ b/src/poetry/utils/cache.py @@ -8,11 +8,21 @@ import time from pathlib import Path +from typing import TYPE_CHECKING from typing import Any from typing import Callable from typing import Generic from typing import TypeVar +from poetry.utils.wheel import InvalidWheelName +from poetry.utils.wheel import Wheel + + +if TYPE_CHECKING: + from poetry.core.packages.utils.link import Link + + from poetry.utils.env import Env + # Used by Cachy for items that do not expire. MAX_DATE = 9999999999 @@ -196,3 +206,83 @@ def _deserialize(self, data_raw: bytes) -> CacheItem[T]: data = json.loads(data_str[10:]) expires = int(data_str[:10]) return CacheItem(data, expires) + + +class ArtifactCache: + def __init__(self, *, cache_dir: Path) -> None: + self._cache_dir = cache_dir + + def get_cache_directory_for_link(self, link: Link) -> Path: + key_parts = {"url": link.url_without_fragment} + + if link.hash_name is not None and link.hash is not None: + key_parts[link.hash_name] = link.hash + + if link.subdirectory_fragment: + key_parts["subdirectory"] = link.subdirectory_fragment + + key = hashlib.sha256( + json.dumps( + key_parts, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("ascii") + ).hexdigest() + + split_key = [key[:2], key[2:4], key[4:6], key[6:]] + + return self._cache_dir.joinpath(*split_key) + + def get_cached_archive_for_link( + self, + link: Link, + *, + strict: bool, + env: Env | None = None, + ) -> Path | None: + assert strict or env is not None + + archives = self._get_cached_archives_for_link(link) + if not archives: + return None + + candidates: list[tuple[float | None, Path]] = [] + for archive in archives: + if strict: + # in strict mode return the original cached archive instead of the + # prioritized archive type. + if link.filename == archive.name: + return archive + continue + + assert env is not None + + if archive.suffix != ".whl": + candidates.append((float("inf"), archive)) + continue + + try: + wheel = Wheel(archive.name) + except InvalidWheelName: + continue + + if not wheel.is_supported_by_environment(env): + continue + + candidates.append( + (wheel.get_minimum_supported_index(env.supported_tags), archive), + ) + + if not candidates: + return None + + return min(candidates)[1] + + def _get_cached_archives_for_link(self, link: Link) -> list[Path]: + cache_dir = self.get_cache_directory_for_link(link) + + archive_types = ["whl", "tar.gz", "tar.bz2", "bz2", "zip"] + paths = [] + for archive_type in archive_types: + for archive in cache_dir.glob(f"*.{archive_type}"): + paths.append(Path(archive)) + + return paths diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 8d897fd2ac9..d5755d152c3 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -1533,6 +1533,7 @@ def _run(self, cmd: list[str], **kwargs: Any) -> int | str: stderr=stderr, input=encode(input_), check=True, + env=env, **kwargs, ).stdout elif call: diff --git a/src/poetry/utils/wheel.py b/src/poetry/utils/wheel.py new file mode 100644 index 00000000000..f45c50b3b35 --- /dev/null +++ b/src/poetry/utils/wheel.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import logging + +from typing import TYPE_CHECKING + +from packaging.tags import Tag + +from poetry.utils.patterns import wheel_file_re + + +if TYPE_CHECKING: + from poetry.utils.env import Env + + +logger = logging.getLogger(__name__) + + +class InvalidWheelName(Exception): + pass + + +class Wheel: + def __init__(self, filename: str) -> None: + wheel_info = wheel_file_re.match(filename) + if not wheel_info: + raise InvalidWheelName(f"{filename} is not a valid wheel filename.") + + self.filename = filename + self.name = wheel_info.group("name").replace("_", "-") + self.version = wheel_info.group("ver").replace("_", "-") + self.build_tag = wheel_info.group("build") + self.pyversions = wheel_info.group("pyver").split(".") + self.abis = wheel_info.group("abi").split(".") + self.plats = wheel_info.group("plat").split(".") + + self.tags = { + Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats + } + + def get_minimum_supported_index(self, tags: list[Tag]) -> int | None: + indexes = [tags.index(t) for t in self.tags if t in tags] + + return min(indexes) if indexes else None + + def is_supported_by_environment(self, env: Env) -> bool: + return bool(set(env.supported_tags).intersection(self.tags)) diff --git a/tests/fixtures/build_system_requires_not_available/README.rst b/tests/fixtures/build_system_requires_not_available/README.rst new file mode 100644 index 00000000000..f7fe15470f9 --- /dev/null +++ b/tests/fixtures/build_system_requires_not_available/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/fixtures/build_system_requires_not_available/pyproject.toml b/tests/fixtures/build_system_requires_not_available/pyproject.toml new file mode 100644 index 00000000000..bfe752e82ba --- /dev/null +++ b/tests/fixtures/build_system_requires_not_available/pyproject.toml @@ -0,0 +1,29 @@ +[tool.poetry] +name = "simple-project" +version = "1.2.3" +description = "Some description." +authors = [ + "Sébastien Eustace " +] +license = "MIT" + +readme = ["README.rst"] + +homepage = "https://python-poetry.org" +repository = "https://github.com/python-poetry/poetry" +documentation = "https://python-poetry.org/docs" + +keywords = ["packaging", "dependency", "poetry"] + +classifiers = [ + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Libraries :: Python Modules" +] + +# Requirements +[tool.poetry.dependencies] +python = "^3.7" + +[build-system] +requires = ["poetry-core==0.999"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/fixtures/build_system_requires_not_available/simple_project/__init__.py b/tests/fixtures/build_system_requires_not_available/simple_project/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl b/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl index a01175c144e..9f1ce9264e8 100644 Binary files a/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl and b/tests/fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl differ diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py index 19283cdeaaa..c03dfd07c4d 100644 --- a/tests/installation/test_chef.py +++ b/tests/installation/test_chef.py @@ -9,14 +9,13 @@ import pytest -from packaging.tags import Tag from poetry.core.packages.utils.link import Link from poetry.factory import Factory from poetry.installation.chef import Chef from poetry.repositories import RepositoryPool +from poetry.utils.cache import ArtifactCache from poetry.utils.env import EnvManager -from poetry.utils.env import MockEnv from tests.repositories.test_pypi_repository import MockRepository @@ -24,6 +23,7 @@ from pytest_mock import MockerFixture from tests.conftest import Config + from tests.types import FixtureDirGetter @pytest.fixture() @@ -40,166 +40,22 @@ def setup(mocker: MockerFixture, pool: RepositoryPool) -> None: mocker.patch.object(Factory, "create_pool", return_value=pool) -@pytest.mark.parametrize( - ("link", "strict", "available_packages"), - [ - ( - "https://files.python-poetry.org/demo-0.1.0.tar.gz", - True, - [ - Path("/cache/demo-0.1.0-py2.py3-none-any"), - Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), - Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), - ], - ), - ( - "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - False, - [], - ), - ], -) -def test_get_not_found_cached_archive_for_link( - config: Config, - mocker: MockerFixture, - link: str, - strict: bool, - available_packages: list[Path], -): - chef = Chef( - config, - MockEnv( - version_info=(3, 8, 3), - marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, - supported_tags=[ - Tag("cp38", "cp38", "macosx_10_15_x86_64"), - Tag("py3", "none", "any"), - ], - ), - Factory.create_pool(config), - ) - - mocker.patch.object( - chef, "get_cached_archives_for_link", return_value=available_packages - ) - - archive = chef.get_cached_archive_for_link(Link(link), strict=strict) - - assert archive is None - - -@pytest.mark.parametrize( - ("link", "cached", "strict"), - [ - ( - "https://files.python-poetry.org/demo-0.1.0.tar.gz", - "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - False, - ), - ( - "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - False, - ), - ( - "https://files.python-poetry.org/demo-0.1.0.tar.gz", - "/cache/demo-0.1.0.tar.gz", - True, - ), - ( - "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", - True, - ), - ], -) -def test_get_found_cached_archive_for_link( - config: Config, mocker: MockerFixture, link: str, cached: str, strict: bool -): - chef = Chef( - config, - MockEnv( - version_info=(3, 8, 3), - marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, - supported_tags=[ - Tag("cp38", "cp38", "macosx_10_15_x86_64"), - Tag("py3", "none", "any"), - ], - ), - Factory.create_pool(config), - ) - - mocker.patch.object( - chef, - "get_cached_archives_for_link", - return_value=[ - Path("/cache/demo-0.1.0-py2.py3-none-any"), - Path("/cache/demo-0.1.0.tar.gz"), - Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), - Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), - ], - ) +@pytest.fixture +def artifact_cache(config: Config) -> ArtifactCache: + return ArtifactCache(cache_dir=config.artifacts_cache_directory) - archive = chef.get_cached_archive_for_link(Link(link), strict=strict) - assert Path(cached) == archive - - -def test_get_cached_archives_for_link(config: Config, mocker: MockerFixture): - chef = Chef( - config, - MockEnv( - marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"} - ), - Factory.create_pool(config), - ) - - distributions = Path(__file__).parent.parent.joinpath("fixtures/distributions") - mocker.patch.object( - chef, - "get_cache_directory_for_link", - return_value=distributions, - ) - - archives = chef.get_cached_archives_for_link( - Link("https://files.python-poetry.org/demo-0.1.0.tar.gz") - ) - - assert archives - assert set(archives) == set(distributions.glob("demo-0.1.*")) - - -def test_get_cache_directory_for_link(config: Config, config_cache_dir: Path): +def test_prepare_sdist( + config: Config, + config_cache_dir: Path, + artifact_cache: ArtifactCache, + fixture_dir: FixtureDirGetter, +) -> None: chef = Chef( - config, - MockEnv( - marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"} - ), - Factory.create_pool(config), - ) - - directory = chef.get_cache_directory_for_link( - Link("https://files.python-poetry.org/poetry-1.1.0.tar.gz") - ) - - expected = Path( - f"{config_cache_dir.as_posix()}/artifacts/ba/63/13/" - "283a3b3b7f95f05e9e6f84182d276f7bb0951d5b0cc24422b33f7a4648" - ) - - assert directory == expected - - -def test_prepare_sdist(config: Config, config_cache_dir: Path) -> None: - chef = Chef(config, EnvManager.get_system_env(), Factory.create_pool(config)) - - archive = ( - Path(__file__) - .parent.parent.joinpath("fixtures/distributions/demo-0.1.0.tar.gz") - .resolve() + artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) ) - - destination = chef.get_cache_directory_for_link(Link(archive.as_uri())) + archive = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve() + destination = artifact_cache.get_cache_directory_for_link(Link(archive.as_uri())) wheel = chef.prepare(archive) @@ -207,10 +63,16 @@ def test_prepare_sdist(config: Config, config_cache_dir: Path) -> None: assert wheel.name == "demo-0.1.0-py3-none-any.whl" -def test_prepare_directory(config: Config, config_cache_dir: Path): - chef = Chef(config, EnvManager.get_system_env(), Factory.create_pool(config)) - - archive = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve() +def test_prepare_directory( + config: Config, + config_cache_dir: Path, + artifact_cache: ArtifactCache, + fixture_dir: FixtureDirGetter, +): + chef = Chef( + artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) + ) + archive = fixture_dir("simple_project").resolve() wheel = chef.prepare(archive) @@ -222,16 +84,14 @@ def test_prepare_directory(config: Config, config_cache_dir: Path): def test_prepare_directory_with_extensions( - config: Config, config_cache_dir: Path + config: Config, + config_cache_dir: Path, + artifact_cache: ArtifactCache, + fixture_dir: FixtureDirGetter, ) -> None: env = EnvManager.get_system_env() - chef = Chef(config, env, Factory.create_pool(config)) - - archive = ( - Path(__file__) - .parent.parent.joinpath("fixtures/extended_with_no_setup") - .resolve() - ) + chef = Chef(artifact_cache, env, Factory.create_pool(config)) + archive = fixture_dir("extended_with_no_setup").resolve() wheel = chef.prepare(archive) @@ -242,10 +102,16 @@ def test_prepare_directory_with_extensions( os.unlink(wheel) -def test_prepare_directory_editable(config: Config, config_cache_dir: Path): - chef = Chef(config, EnvManager.get_system_env(), Factory.create_pool(config)) - - archive = Path(__file__).parent.parent.joinpath("fixtures/simple_project").resolve() +def test_prepare_directory_editable( + config: Config, + config_cache_dir: Path, + artifact_cache: ArtifactCache, + fixture_dir: FixtureDirGetter, +): + chef = Chef( + artifact_cache, EnvManager.get_system_env(), Factory.create_pool(config) + ) + archive = fixture_dir("simple_project").resolve() wheel = chef.prepare(archive, editable=True) diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index 578b270354b..474aafab500 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -22,6 +22,7 @@ from cleo.io.outputs.output import Verbosity from poetry.core.packages.package import Package from poetry.core.packages.utils.link import Link +from poetry.core.packages.utils.utils import path_to_url from poetry.factory import Factory from poetry.installation.chef import Chef as BaseChef @@ -31,6 +32,7 @@ from poetry.installation.operations import Update from poetry.installation.wheel_installer import WheelInstaller from poetry.repositories.repository_pool import RepositoryPool +from poetry.utils.cache import ArtifactCache from poetry.utils.env import MockEnv from tests.repositories.test_pypi_repository import MockRepository @@ -92,7 +94,7 @@ def env(tmp_dir: str) -> MockEnv: return MockEnv(path=path, is_venv=True) -@pytest.fixture() +@pytest.fixture def io() -> BufferedIO: io = BufferedIO() io.output.formatter.set_style("c1_dark", Style("cyan", options=["dark"])) @@ -103,7 +105,7 @@ def io() -> BufferedIO: return io -@pytest.fixture() +@pytest.fixture def io_decorated() -> BufferedIO: io = BufferedIO(decorated=True) io.output.formatter.set_style("c1", Style("cyan")) @@ -112,14 +114,14 @@ def io_decorated() -> BufferedIO: return io -@pytest.fixture() +@pytest.fixture def io_not_decorated() -> BufferedIO: io = BufferedIO(decorated=False) return io -@pytest.fixture() +@pytest.fixture def pool() -> RepositoryPool: pool = RepositoryPool() pool.add_repository(MockRepository()) @@ -127,8 +129,15 @@ def pool() -> RepositoryPool: return pool -@pytest.fixture() -def mock_file_downloads(http: type[httpretty.httpretty]) -> None: +@pytest.fixture +def artifact_cache(config: Config) -> ArtifactCache: + return ArtifactCache(cache_dir=config.artifacts_cache_directory) + + +@pytest.fixture +def mock_file_downloads( + http: type[httpretty.httpretty], fixture_dir: FixtureDirGetter +) -> None: def callback( request: HTTPrettyRequest, uri: str, headers: dict[str, Any] ) -> list[int | dict[str, Any] | str]: @@ -140,12 +149,10 @@ def callback( if not fixture.exists(): if name == "demo-0.1.0.tar.gz": - fixture = Path(__file__).parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0.tar.gz" - ) + fixture = fixture_dir("distributions") / "demo-0.1.0.tar.gz" else: - fixture = Path(__file__).parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" + fixture = ( + fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" ) return [200, headers, fixture.read_bytes()] @@ -157,32 +164,25 @@ def callback( ) -@pytest.fixture() -def copy_wheel(tmp_dir: Path) -> Callable[[], Path]: +@pytest.fixture +def copy_wheel(tmp_dir: Path, fixture_dir: FixtureDirGetter) -> Callable[[], Path]: def _copy_wheel() -> Path: tmp_name = tempfile.mktemp() Path(tmp_dir).joinpath(tmp_name).mkdir() shutil.copyfile( - Path(__file__) - .parent.parent.joinpath( - "fixtures/distributions/demo-0.1.2-py2.py3-none-any.whl" - ) - .as_posix(), - Path(tmp_dir) - .joinpath(tmp_name) - .joinpath("demo-0.1.2-py2.py3-none-any.whl") - .as_posix(), + ( + fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl" + ).as_posix(), + (Path(tmp_dir) / tmp_name / "demo-0.1.2-py2.py3-none-any.whl").as_posix(), ) - return ( - Path(tmp_dir).joinpath(tmp_name).joinpath("demo-0.1.2-py2.py3-none-any.whl") - ) + return Path(tmp_dir) / tmp_name / "demo-0.1.2-py2.py3-none-any.whl" return _copy_wheel -@pytest.fixture() +@pytest.fixture def wheel(copy_wheel: Callable[[], Path]) -> Path: archive = copy_wheel() @@ -201,13 +201,15 @@ def test_execute_executes_a_batch_of_operations( mock_file_downloads: None, env: MockEnv, copy_wheel: Callable[[], Path], + fixture_dir: FixtureDirGetter, ): wheel_install = mocker.patch.object(WheelInstaller, "install") config.merge({"cache-dir": tmp_dir}) + artifact_cache = ArtifactCache(cache_dir=config.artifacts_cache_directory) prepare_spy = mocker.spy(Chef, "_prepare") - chef = Chef(config, env, Factory.create_pool(config)) + chef = Chef(artifact_cache, env, Factory.create_pool(config)) chef.set_directory_wheel([copy_wheel(), copy_wheel()]) chef.set_sdist_wheel(copy_wheel()) @@ -220,10 +222,7 @@ def test_execute_executes_a_batch_of_operations( "demo", "0.1.0", source_type="file", - source_url=Path(__file__) - .parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" - ) + source_url=(fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl") .resolve() .as_posix(), ) @@ -232,10 +231,7 @@ def test_execute_executes_a_batch_of_operations( "simple-project", "1.2.3", source_type="directory", - source_url=Path(__file__) - .parent.parent.joinpath("fixtures/simple_project") - .resolve() - .as_posix(), + source_url=fixture_dir("simple_project").resolve().as_posix(), ) git_package = Package( @@ -526,10 +522,9 @@ def test_executor_should_delete_incomplete_downloads( pool: RepositoryPool, mock_file_downloads: None, env: MockEnv, + fixture_dir: FixtureDirGetter, ): - fixture = Path(__file__).parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" - ) + fixture = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" destination_fixture = Path(tmp_dir) / "tomlkit-0.5.3-py2.py3-none-any.whl" shutil.copyfile(str(fixture), str(destination_fixture)) mocker.patch( @@ -537,11 +532,11 @@ def test_executor_should_delete_incomplete_downloads( side_effect=Exception("Download error"), ) mocker.patch( - "poetry.installation.chef.Chef.get_cached_archive_for_link", - side_effect=lambda link, strict: None, + "poetry.installation.executor.ArtifactCache.get_cached_archive_for_link", + return_value=None, ) mocker.patch( - "poetry.installation.chef.Chef.get_cache_directory_for_link", + "poetry.installation.executor.ArtifactCache.get_cache_directory_for_link", return_value=Path(tmp_dir), ) @@ -623,15 +618,13 @@ def test_executor_should_not_write_pep610_url_references_for_cached_package( def test_executor_should_write_pep610_url_references_for_wheel_files( - tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + io: BufferedIO, + fixture_dir: FixtureDirGetter, ): - url = ( - Path(__file__) - .parent.parent.joinpath( - "fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" - ) - .resolve() - ) + url = (fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl").resolve() package = Package("demo", "0.1.0", source_type="file", source_url=url.as_posix()) # Set package.files so the executor will attempt to hash the package package.files = [ @@ -657,13 +650,13 @@ def test_executor_should_write_pep610_url_references_for_wheel_files( def test_executor_should_write_pep610_url_references_for_non_wheel_files( - tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, io: BufferedIO + tmp_venv: VirtualEnv, + pool: RepositoryPool, + config: Config, + io: BufferedIO, + fixture_dir: FixtureDirGetter, ): - url = ( - Path(__file__) - .parent.parent.joinpath("fixtures/distributions/demo-0.1.0.tar.gz") - .resolve() - ) + url = (fixture_dir("distributions") / "demo-0.1.0.tar.gz").resolve() package = Package("demo", "0.1.0", source_type="file", source_url=url.as_posix()) # Set package.files so the executor will attempt to hash the package package.files = [ @@ -692,19 +685,17 @@ def test_executor_should_write_pep610_url_references_for_directories( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, + artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, + fixture_dir: FixtureDirGetter, ): - url = ( - Path(__file__) - .parent.parent.joinpath("fixtures/git/github.com/demo/demo") - .resolve() - ) + url = (fixture_dir("git") / "github.com" / "demo" / "demo").resolve() package = Package( "demo", "0.1.2", source_type="directory", source_url=url.as_posix() ) - chef = Chef(config, tmp_venv, Factory.create_pool(config)) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) executor = Executor(tmp_venv, pool, config, io) @@ -719,14 +710,12 @@ def test_executor_should_write_pep610_url_references_for_editable_directories( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, + artifact_cache: ArtifactCache, io: BufferedIO, wheel: Path, + fixture_dir: FixtureDirGetter, ): - url = ( - Path(__file__) - .parent.parent.joinpath("fixtures/git/github.com/demo/demo") - .resolve() - ) + url = (fixture_dir("git") / "github.com" / "demo" / "demo").resolve() package = Package( "demo", "0.1.2", @@ -735,7 +724,7 @@ def test_executor_should_write_pep610_url_references_for_editable_directories( develop=True, ) - chef = Chef(config, tmp_venv, Factory.create_pool(config)) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) executor = Executor(tmp_venv, pool, config, io) @@ -760,7 +749,7 @@ def test_executor_should_write_pep610_url_references_for_wheel_urls( if is_artifact_cached: link_cached = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" mocker.patch( - "poetry.installation.chef.Chef.get_cached_archive_for_link", + "poetry.installation.executor.ArtifactCache.get_cached_archive_for_link", return_value=link_cached, ) download_spy = mocker.spy(Executor, "_download_archive") @@ -839,7 +828,7 @@ def test_executor_should_write_pep610_url_references_for_non_wheel_urls( cached_sdist = fixture_dir("distributions") / "demo-0.1.0.tar.gz" cached_wheel = fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl" - def mock_get_cached_archive_for_link_func(_: Link, strict: bool): + def mock_get_cached_archive_for_link_func(_: Link, *, strict: bool, **__: Any): if is_wheel_cached and not strict: return cached_wheel if is_sdist_cached: @@ -847,7 +836,7 @@ def mock_get_cached_archive_for_link_func(_: Link, strict: bool): return None mocker.patch( - "poetry.installation.chef.Chef.get_cached_archive_for_link", + "poetry.installation.executor.ArtifactCache.get_cached_archive_for_link", side_effect=mock_get_cached_archive_for_link_func, ) @@ -897,6 +886,7 @@ def test_executor_should_write_pep610_url_references_for_git( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, + artifact_cache: ArtifactCache, io: BufferedIO, mock_file_downloads: None, wheel: Path, @@ -910,7 +900,7 @@ def test_executor_should_write_pep610_url_references_for_git( source_url="https://github.com/demo/demo.git", ) - chef = Chef(config, tmp_venv, Factory.create_pool(config)) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) executor = Executor(tmp_venv, pool, config, io) @@ -935,6 +925,7 @@ def test_executor_should_append_subdirectory_for_git( tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, + artifact_cache: ArtifactCache, io: BufferedIO, mock_file_downloads: None, wheel: Path, @@ -949,7 +940,7 @@ def test_executor_should_append_subdirectory_for_git( source_subdirectory="two", ) - chef = Chef(config, tmp_venv, Factory.create_pool(config)) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) spy = mocker.spy(chef, "prepare") @@ -965,6 +956,7 @@ def test_executor_should_write_pep610_url_references_for_git_with_subdirectories tmp_venv: VirtualEnv, pool: RepositoryPool, config: Config, + artifact_cache: ArtifactCache, io: BufferedIO, mock_file_downloads: None, wheel: Path, @@ -979,7 +971,7 @@ def test_executor_should_write_pep610_url_references_for_git_with_subdirectories source_subdirectory="two", ) - chef = Chef(config, tmp_venv, Factory.create_pool(config)) + chef = Chef(artifact_cache, tmp_venv, Factory.create_pool(config)) chef.set_directory_wheel(wheel) executor = Executor(tmp_venv, pool, config, io) @@ -1039,6 +1031,7 @@ def test_executor_fallback_on_poetry_create_error_without_wheel_installer( tmp_dir: str, mock_file_downloads: None, env: MockEnv, + fixture_dir: FixtureDirGetter, ): mock_pip_install = mocker.patch("poetry.installation.executor.pip_install") mock_sdist_builder = mocker.patch("poetry.core.masonry.builders.sdist.SdistBuilder") @@ -1062,10 +1055,7 @@ def test_executor_fallback_on_poetry_create_error_without_wheel_installer( "simple-project", "1.2.3", source_type="directory", - source_url=Path(__file__) - .parent.parent.joinpath("fixtures/simple_project") - .resolve() - .as_posix(), + source_url=fixture_dir("simple_project").resolve().as_posix(), ) return_code = executor.execute( @@ -1093,8 +1083,10 @@ def test_executor_fallback_on_poetry_create_error_without_wheel_installer( @pytest.mark.parametrize("failing_method", ["build", "get_requires_for_build"]) +@pytest.mark.parametrize("editable", [False, True]) def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( failing_method: str, + editable: bool, mocker: MockerFixture, config: Config, pool: RepositoryPool, @@ -1102,7 +1094,8 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( tmp_dir: str, mock_file_downloads: None, env: MockEnv, -): + fixture_dir: FixtureDirGetter, +) -> None: error = BuildBackendException( CalledProcessError(1, ["pip"], output=b"Error on stdout") ) @@ -1117,11 +1110,11 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( package_name, package_version, source_type="directory", - source_url=Path(__file__) - .parent.parent.joinpath("fixtures/simple_project") - .resolve() - .as_posix(), + source_url=fixture_dir("simple_project").resolve().as_posix(), + develop=editable, ) + # must not be included in the error message + directory_package.python_versions = ">=3.7" return_code = executor.execute( [ @@ -1145,14 +1138,70 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess( Error on stdout """ - requirement = directory_package.to_dependency().to_pep_508() + if editable: + pip_command = "pip wheel --use-pep517 --editable" + requirement = directory_package.source_url + assert Path(requirement).exists() + else: + pip_command = "pip wheel --use-pep517" + requirement = f"{package_name} @ {path_to_url(directory_package.source_url)}" expected_end = f""" Note: This error originates from the build backend, and is likely not a problem with \ poetry but with {package_name} ({package_version} {package_url}) not supporting \ -PEP 517 builds. You can verify this by running 'pip wheel --use-pep517 "{requirement}"'. +PEP 517 builds. You can verify this by running '{pip_command} "{requirement}"'. """ output = io.fetch_output() assert output.startswith(expected_start) assert output.endswith(expected_end) + + +def test_build_system_requires_not_available( + config: Config, + pool: RepositoryPool, + io: BufferedIO, + tmp_dir: str, + mock_file_downloads: None, + env: MockEnv, + fixture_dir: FixtureDirGetter, +) -> None: + io.set_verbosity(Verbosity.NORMAL) + + executor = Executor(env, pool, config, io) + + package_name = "simple-project" + package_version = "1.2.3" + directory_package = Package( + package_name, + package_version, + source_type="directory", + source_url=fixture_dir("build_system_requires_not_available") + .resolve() + .as_posix(), + ) + + return_code = executor.execute( + [ + Install(directory_package), + ] + ) + + assert return_code == 1 + + package_url = directory_package.source_url + expected_start = f"""\ +Package operations: 1 install, 0 updates, 0 removals + + • Installing {package_name} ({package_version} {package_url}) + + SolveFailure + + Because -root- depends on poetry-core (0.999) which doesn't match any versions,\ + version solving failed. +""" + expected_end = "Cannot resolve build-system.requires for simple-project." + + output = io.fetch_output().strip() + assert output.startswith(expected_start) + assert output.endswith(expected_end) diff --git a/tests/installation/test_wheel_installer.py b/tests/installation/test_wheel_installer.py index 7fc4826f940..b7b3d7c7c93 100644 --- a/tests/installation/test_wheel_installer.py +++ b/tests/installation/test_wheel_installer.py @@ -77,5 +77,7 @@ def test_enable_bytecode_compilation( if compile: assert cache_dir.exists() assert list(cache_dir.glob("*.pyc")) + assert not list(cache_dir.glob("*.opt-1.pyc")) + assert not list(cache_dir.glob("*.opt-2.pyc")) else: assert not cache_dir.exists() diff --git a/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl index c3a868e5caa..4eb8ce84187 100644 Binary files a/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl and b/tests/repositories/fixtures/pypi.org/dists/pytest-3.5.1-py2.py3-none-any.whl differ diff --git a/tests/utils/test_cache.py b/tests/utils/test_cache.py index c1bbae5071a..8cdbc93a284 100644 --- a/tests/utils/test_cache.py +++ b/tests/utils/test_cache.py @@ -1,5 +1,6 @@ from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import TypeVar @@ -9,18 +10,21 @@ import pytest from cachy import CacheManager +from packaging.tags import Tag +from poetry.core.packages.utils.link import Link +from poetry.utils.cache import ArtifactCache from poetry.utils.cache import FileCache +from poetry.utils.env import MockEnv if TYPE_CHECKING: - from pathlib import Path - from _pytest.monkeypatch import MonkeyPatch from pytest import FixtureRequest from pytest_mock import MockerFixture from tests.conftest import Config + from tests.types import FixtureDirGetter FILE_CACHE = Union[FileCache, CacheManager] @@ -192,3 +196,139 @@ def test_cachy_compatibility( assert cachy_file_cache.get("key3") == test_str assert cachy_file_cache.get("key4") == test_obj + + +def test_get_cache_directory_for_link(tmp_path: Path) -> None: + cache = ArtifactCache(cache_dir=tmp_path) + directory = cache.get_cache_directory_for_link( + Link("https://files.python-poetry.org/poetry-1.1.0.tar.gz") + ) + + expected = Path( + f"{tmp_path.as_posix()}/11/4f/a8/" + "1c89d75547e4967082d30a28360401c82c83b964ddacee292201bf85f2" + ) + + assert directory == expected + + +def test_get_cached_archives_for_link( + fixture_dir: FixtureDirGetter, mocker: MockerFixture +) -> None: + distributions = fixture_dir("distributions") + cache = ArtifactCache(cache_dir=Path()) + + mocker.patch.object( + cache, + "get_cache_directory_for_link", + return_value=distributions, + ) + archives = cache._get_cached_archives_for_link( + Link("https://files.python-poetry.org/demo-0.1.0.tar.gz") + ) + + assert archives + assert set(archives) == set(distributions.glob("demo-0.1.*")) + + +@pytest.mark.parametrize( + ("link", "strict", "available_packages"), + [ + ( + "https://files.python-poetry.org/demo-0.1.0.tar.gz", + True, + [ + Path("/cache/demo-0.1.0-py2.py3-none-any"), + Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), + Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), + ], + ), + ( + "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + False, + [], + ), + ], +) +def test_get_not_found_cached_archive_for_link( + mocker: MockerFixture, + link: str, + strict: bool, + available_packages: list[Path], +) -> None: + env = MockEnv( + version_info=(3, 8, 3), + marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, + supported_tags=[ + Tag("cp38", "cp38", "macosx_10_15_x86_64"), + Tag("py3", "none", "any"), + ], + ) + cache = ArtifactCache(cache_dir=Path()) + + mocker.patch.object( + cache, + "_get_cached_archives_for_link", + return_value=available_packages, + ) + + archive = cache.get_cached_archive_for_link(Link(link), strict=strict, env=env) + + assert archive is None + + +@pytest.mark.parametrize( + ("link", "cached", "strict"), + [ + ( + "https://files.python-poetry.org/demo-0.1.0.tar.gz", + "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + False, + ), + ( + "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + False, + ), + ( + "https://files.python-poetry.org/demo-0.1.0.tar.gz", + "/cache/demo-0.1.0.tar.gz", + True, + ), + ( + "https://example.com/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + "/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl", + True, + ), + ], +) +def test_get_found_cached_archive_for_link( + mocker: MockerFixture, + link: str, + cached: str, + strict: bool, +) -> None: + env = MockEnv( + version_info=(3, 8, 3), + marker_env={"interpreter_name": "cpython", "interpreter_version": "3.8.3"}, + supported_tags=[ + Tag("cp38", "cp38", "macosx_10_15_x86_64"), + Tag("py3", "none", "any"), + ], + ) + cache = ArtifactCache(cache_dir=Path()) + + mocker.patch.object( + cache, + "_get_cached_archives_for_link", + return_value=[ + Path("/cache/demo-0.1.0-py2.py3-none-any"), + Path("/cache/demo-0.1.0.tar.gz"), + Path("/cache/demo-0.1.0-cp38-cp38-macosx_10_15_x86_64.whl"), + Path("/cache/demo-0.1.0-cp37-cp37-macosx_10_15_x86_64.whl"), + ], + ) + + archive = cache.get_cached_archive_for_link(Link(link), strict=strict, env=env) + + assert Path(cached) == archive