diff --git a/src/poetry/console/commands/debug/resolve.py b/src/poetry/console/commands/debug/resolve.py index 05cf4b15736..ec168f6adf8 100644 --- a/src/poetry/console/commands/debug/resolve.py +++ b/src/poetry/console/commands/debug/resolve.py @@ -113,7 +113,7 @@ def handle(self) -> int: if self.option("install"): env = EnvManager(self.poetry).get() - pool = RepositoryPool() + pool = RepositoryPool(config=self.poetry.config) locked_repository = Repository("poetry-locked") for op in ops: locked_repository.add_package(op.package) diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index 9204c487f38..54c0af92bd7 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -13,7 +13,7 @@ from poetry.console.commands.command import Command from poetry.console.commands.env_command import EnvCommand -from poetry.utils.dependency_specification import parse_dependency_specification +from poetry.utils.dependency_specification import RequirementsParser if TYPE_CHECKING: @@ -434,17 +434,17 @@ def _parse_requirements(self, requirements: list[str]) -> list[dict[str, Any]]: try: cwd = self.poetry.file.parent + artifact_cache = self.poetry.pool.artifact_cache except (PyProjectException, RuntimeError): cwd = Path.cwd() + artifact_cache = self._get_pool().artifact_cache - return [ - parse_dependency_specification( - requirement=requirement, - env=self.env if isinstance(self, EnvCommand) else None, - cwd=cwd, - ) - for requirement in requirements - ] + parser = RequirementsParser( + artifact_cache=artifact_cache, + env=self.env if isinstance(self, EnvCommand) else None, + cwd=cwd, + ) + return [parser.parse(requirement) for requirement in requirements] def _format_requirements(self, requirements: list[dict[str, str]]) -> Requirements: requires: Requirements = {} diff --git a/src/poetry/console/commands/show.py b/src/poetry/console/commands/show.py index 03cab868a10..c8c1fbc3595 100644 --- a/src/poetry/console/commands/show.py +++ b/src/poetry/console/commands/show.py @@ -213,7 +213,7 @@ def _display_packages_information( from poetry.utils.helpers import get_package_version_display_string locked_packages = locked_repository.packages - pool = RepositoryPool(ignore_repository_names=True) + pool = RepositoryPool(ignore_repository_names=True, config=self.poetry.config) pool.add_repository(locked_repository) solver = Solver( root, diff --git a/src/poetry/factory.py b/src/poetry/factory.py index 68c51bdf2c3..9c2c92c93b2 100644 --- a/src/poetry/factory.py +++ b/src/poetry/factory.py @@ -119,7 +119,7 @@ def get_package(cls, name: str, version: str) -> ProjectPackage: @classmethod def create_pool( cls, - auth_config: Config, + config: Config, sources: Iterable[dict[str, Any]] = (), io: IO | None = None, disable_cache: bool = False, @@ -133,12 +133,12 @@ def create_pool( if disable_cache: logger.debug("Disabling source caches") - pool = RepositoryPool() + pool = RepositoryPool(config=config) explicit_pypi = False for source in sources: repository = cls.create_package_source( - source, auth_config, disable_cache=disable_cache + source, config, disable_cache=disable_cache ) priority = Priority[source.get("priority", Priority.PRIMARY.name).upper()] if "default" in source or "secondary" in source: @@ -207,7 +207,7 @@ def create_pool( @classmethod def create_package_source( - cls, source: dict[str, str], auth_config: Config, disable_cache: bool = False + cls, source: dict[str, str], config: Config, disable_cache: bool = False ) -> HTTPRepository: from poetry.repositories.exceptions import InvalidSourceError from poetry.repositories.legacy_repository import LegacyRepository @@ -239,7 +239,7 @@ def create_package_source( return repository_class( name, url, - config=auth_config, + config=config, disable_cache=disable_cache, ) diff --git a/src/poetry/installation/installer.py b/src/poetry/installation/installer.py index 45c3893bc55..fea3cc96586 100644 --- a/src/poetry/installation/installer.py +++ b/src/poetry/installation/installer.py @@ -47,6 +47,7 @@ def __init__( self._package = package self._locker = locker self._pool = pool + self._config = config self._dry_run = False self._requires_synchronization = False @@ -290,7 +291,7 @@ def _do_install(self) -> int: ) # We resolve again by only using the lock file - pool = RepositoryPool(ignore_repository_names=True) + pool = RepositoryPool(ignore_repository_names=True, config=self._config) # Making a new repo containing the packages # newly resolved and the ones from the current lock file diff --git a/src/poetry/packages/direct_origin.py b/src/poetry/packages/direct_origin.py new file mode 100644 index 00000000000..cdc16d55c49 --- /dev/null +++ b/src/poetry/packages/direct_origin.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import functools +import os +import urllib.parse + +from pathlib import Path +from typing import TYPE_CHECKING + +from poetry.core.packages.utils.link import Link + +from poetry.inspection.info import PackageInfo +from poetry.inspection.info import PackageInfoError +from poetry.utils.helpers import download_file +from poetry.utils.helpers import get_file_hash +from poetry.vcs.git import Git + + +if TYPE_CHECKING: + from poetry.core.packages.package import Package + + from poetry.utils.cache import ArtifactCache + + +@functools.lru_cache(maxsize=None) +def _get_package_from_git( + url: str, + branch: str | None = None, + tag: str | None = None, + rev: str | None = None, + subdirectory: str | None = None, + source_root: Path | None = None, +) -> Package: + source = Git.clone( + url=url, + source_root=source_root, + branch=branch, + tag=tag, + revision=rev, + clean=False, + ) + revision = Git.get_revision(source) + + path = Path(source.path) + if subdirectory: + path = path.joinpath(subdirectory) + + package = DirectOrigin.get_package_from_directory(path) + package._source_type = "git" + package._source_url = url + package._source_reference = rev or tag or branch or "HEAD" + package._source_resolved_reference = revision + package._source_subdirectory = subdirectory + + return package + + +class DirectOrigin: + def __init__(self, artifact_cache: ArtifactCache) -> None: + self._artifact_cache = artifact_cache + + @classmethod + def get_package_from_file(cls, file_path: Path) -> Package: + try: + package = PackageInfo.from_path(path=file_path).to_package( + root_dir=file_path + ) + except PackageInfoError: + raise RuntimeError( + f"Unable to determine package info from path: {file_path}" + ) + + return package + + @classmethod + def get_package_from_directory(cls, directory: Path) -> Package: + return PackageInfo.from_directory(path=directory).to_package(root_dir=directory) + + def get_package_from_url(self, url: str) -> Package: + file_name = os.path.basename(urllib.parse.urlparse(url).path) + link = Link(url) + artifact = self._artifact_cache.get_cached_archive_for_link(link, strict=True) + + if not artifact: + artifact = ( + self._artifact_cache.get_cache_directory_for_link(link) / file_name + ) + artifact.parent.mkdir(parents=True, exist_ok=True) + download_file(url, artifact) + + package = self.get_package_from_file(artifact) + package.files = [ + {"file": file_name, "hash": "sha256:" + get_file_hash(artifact)} + ] + + package._source_type = "url" + package._source_url = url + + return package + + @staticmethod + def get_package_from_vcs( + vcs: str, + url: str, + branch: str | None = None, + tag: str | None = None, + rev: str | None = None, + subdirectory: str | None = None, + source_root: Path | None = None, + ) -> Package: + if vcs != "git": + raise ValueError(f"Unsupported VCS dependency {vcs}") + + return _get_package_from_git( + url=url, + branch=branch, + tag=tag, + rev=rev, + subdirectory=subdirectory, + source_root=source_root, + ) diff --git a/src/poetry/poetry.py b/src/poetry/poetry.py index 3cb227eaaaa..57c05b3e540 100644 --- a/src/poetry/poetry.py +++ b/src/poetry/poetry.py @@ -49,7 +49,7 @@ def __init__( self._locker = locker self._config = config - self._pool = RepositoryPool() + self._pool = RepositoryPool(config=config) self._plugin_manager: PluginManager | None = None self._disable_cache = disable_cache diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index facd89c38a3..190a0e31d66 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -1,16 +1,11 @@ from __future__ import annotations -import functools import logging -import os import re -import tempfile import time -import urllib.parse from collections import defaultdict from contextlib import contextmanager -from pathlib import Path from typing import TYPE_CHECKING from typing import cast @@ -22,19 +17,16 @@ from poetry.core.version.markers import EmptyMarker from poetry.core.version.markers import MarkerUnion -from poetry.inspection.info import PackageInfo -from poetry.inspection.info import PackageInfoError from poetry.mixology.incompatibility import Incompatibility from poetry.mixology.incompatibility_cause import DependencyCause from poetry.mixology.incompatibility_cause import PythonCause from poetry.mixology.term import Term from poetry.packages import DependencyPackage +from poetry.packages.direct_origin import DirectOrigin from poetry.packages.package_collection import PackageCollection from poetry.puzzle.exceptions import OverrideNeeded from poetry.repositories.exceptions import PackageNotFound -from poetry.utils.helpers import download_file from poetry.utils.helpers import get_file_hash -from poetry.vcs.git import Git if TYPE_CHECKING: @@ -42,6 +34,7 @@ from collections.abc import Collection from collections.abc import Iterable from collections.abc import Iterator + from pathlib import Path from cleo.io.io import IO from packaging.utils import NormalizedName @@ -100,39 +93,6 @@ def _formatter_elapsed(self) -> str: return f"{elapsed:.1f}s" -@functools.lru_cache(maxsize=None) -def _get_package_from_git( - url: str, - branch: str | None = None, - tag: str | None = None, - rev: str | None = None, - subdirectory: str | None = None, - source_root: Path | None = None, -) -> Package: - source = Git.clone( - url=url, - source_root=source_root, - branch=branch, - tag=tag, - revision=rev, - clean=False, - ) - revision = Git.get_revision(source) - - path = Path(source.path) - if subdirectory: - path = path.joinpath(subdirectory) - - package = Provider.get_package_from_directory(path) - package._source_type = "git" - package._source_url = url - package._source_reference = rev or tag or branch or "HEAD" - package._source_resolved_reference = revision - package._source_subdirectory = subdirectory - - return package - - class Provider: UNSAFE_PACKAGES: set[str] = set() @@ -147,6 +107,7 @@ def __init__( ) -> None: self._package = package self._pool = pool + self._direct_origin = DirectOrigin(self._pool.artifact_cache) self._io = io self._env: Env | None = None self._python_constraint = package.python_constraint @@ -351,7 +312,7 @@ def _search_for_vcs(self, dependency: VCSDependency) -> Package: Basically, we clone the repository in a temporary directory and get the information we need by checking out the specified reference. """ - package = self.get_package_from_vcs( + package = self._direct_origin.get_package_from_vcs( dependency.vcs, dependency.source, branch=dependency.branch, @@ -368,31 +329,9 @@ def _search_for_vcs(self, dependency: VCSDependency) -> Package: return package - @staticmethod - def get_package_from_vcs( - vcs: str, - url: str, - branch: str | None = None, - tag: str | None = None, - rev: str | None = None, - subdirectory: str | None = None, - source_root: Path | None = None, - ) -> Package: - if vcs != "git": - raise ValueError(f"Unsupported VCS dependency {vcs}") - - return _get_package_from_git( - url=url, - branch=branch, - tag=tag, - rev=rev, - subdirectory=subdirectory, - source_root=source_root, - ) - def _search_for_file(self, dependency: FileDependency) -> Package: dependency.validate(raise_error=True) - package = self.get_package_from_file(dependency.full_path) + package = self._direct_origin.get_package_from_file(dependency.full_path) self.validate_package_for_dependency(dependency=dependency, package=package) @@ -408,22 +347,9 @@ def _search_for_file(self, dependency: FileDependency) -> Package: return package - @classmethod - def get_package_from_file(cls, file_path: Path) -> Package: - try: - package = PackageInfo.from_path(path=file_path).to_package( - root_dir=file_path - ) - except PackageInfoError: - raise RuntimeError( - f"Unable to determine package info from path: {file_path}" - ) - - return package - def _search_for_directory(self, dependency: DirectoryDependency) -> Package: dependency.validate(raise_error=True) - package = self.get_package_from_directory(dependency.full_path) + package = self._direct_origin.get_package_from_directory(dependency.full_path) self.validate_package_for_dependency(dependency=dependency, package=package) @@ -434,12 +360,8 @@ def _search_for_directory(self, dependency: DirectoryDependency) -> Package: return package - @classmethod - def get_package_from_directory(cls, directory: Path) -> Package: - return PackageInfo.from_directory(path=directory).to_package(root_dir=directory) - def _search_for_url(self, dependency: URLDependency) -> Package: - package = self.get_package_from_url(dependency.url) + package = self._direct_origin.get_package_from_url(dependency.url) self.validate_package_for_dependency(dependency=dependency, package=package) @@ -453,23 +375,6 @@ def _search_for_url(self, dependency: URLDependency) -> Package: return package - @classmethod - def get_package_from_url(cls, url: str) -> Package: - file_name = os.path.basename(urllib.parse.urlparse(url).path) - with tempfile.TemporaryDirectory() as temp_dir: - dest = Path(temp_dir) / file_name - download_file(url, dest) - package = cls.get_package_from_file(dest) - - package.files = [ - {"file": file_name, "hash": "sha256:" + get_file_hash(dest)} - ] - - package._source_type = "url" - package._source_url = url - - return package - def _get_dependencies_with_overrides( self, dependencies: list[Dependency], package: DependencyPackage ) -> list[Dependency]: diff --git a/src/poetry/repositories/repository_pool.py b/src/poetry/repositories/repository_pool.py index 482a9985503..9b9c5e03260 100644 --- a/src/poetry/repositories/repository_pool.py +++ b/src/poetry/repositories/repository_pool.py @@ -8,8 +8,10 @@ from enum import IntEnum from typing import TYPE_CHECKING +from poetry.config.config import Config from poetry.repositories.abstract_repository import AbstractRepository from poetry.repositories.exceptions import PackageNotFound +from poetry.utils.cache import ArtifactCache if TYPE_CHECKING: @@ -40,6 +42,8 @@ def __init__( self, repositories: list[Repository] | None = None, ignore_repository_names: bool = False, + *, + config: Config | None = None, ) -> None: super().__init__("poetry-repository-pool") self._repositories: OrderedDict[str, PrioritizedRepository] = OrderedDict() @@ -50,6 +54,10 @@ def __init__( for repository in repositories: self.add_repository(repository) + self._artifact_cache = ArtifactCache( + cache_dir=(config or Config.create()).artifacts_cache_directory + ) + @property def repositories(self) -> list[Repository]: """ @@ -77,6 +85,10 @@ def _sorted_repositories(self) -> list[PrioritizedRepository]: self._repositories.values(), key=lambda prio_repo: prio_repo.priority ) + @property + def artifact_cache(self) -> ArtifactCache: + return self._artifact_cache + def has_default(self) -> bool: return self._contains_priority(Priority.DEFAULT) diff --git a/src/poetry/utils/dependency_specification.py b/src/poetry/utils/dependency_specification.py index 0598f000f4f..27f3a0da5e8 100644 --- a/src/poetry/utils/dependency_specification.py +++ b/src/poetry/utils/dependency_specification.py @@ -16,138 +16,17 @@ from poetry.core.packages.dependency import Dependency from tomlkit.items import InlineTable -from poetry.puzzle.provider import Provider +from poetry.packages.direct_origin import DirectOrigin if TYPE_CHECKING: from poetry.core.packages.vcs_dependency import VCSDependency + from poetry.utils.cache import ArtifactCache from poetry.utils.env import Env DependencySpec = Dict[str, Union[str, bool, Dict[str, Union[str, bool]], List[str]]] - - -def _parse_dependency_specification_git_url( - requirement: str, env: Env | None = None -) -> DependencySpec | None: - from poetry.core.vcs.git import Git - from poetry.core.vcs.git import ParsedUrl - - parsed = ParsedUrl.parse(requirement) - url = Git.normalize_url(requirement) - - pair = {"name": parsed.name, "git": url.url} - - if parsed.rev: - pair["rev"] = url.revision - - if parsed.subdirectory: - pair["subdirectory"] = parsed.subdirectory - - source_root = env.path.joinpath("src") if env else None - package = Provider.get_package_from_vcs( - "git", - url=url.url, - rev=pair.get("rev"), - subdirectory=parsed.subdirectory, - source_root=source_root, - ) - pair["name"] = package.name - return pair - - -def _parse_dependency_specification_url( - requirement: str, env: Env | None = None -) -> DependencySpec | None: - url_parsed = urllib.parse.urlparse(requirement) - if not (url_parsed.scheme and url_parsed.netloc): - return None - - if url_parsed.scheme in ["git+https", "git+ssh"]: - return _parse_dependency_specification_git_url(requirement, env) - - if url_parsed.scheme in ["http", "https"]: - package = Provider.get_package_from_url(requirement) - assert package.source_url is not None - return {"name": package.name, "url": package.source_url} - - return None - - -def _parse_dependency_specification_path( - requirement: str, cwd: Path -) -> DependencySpec | None: - if (os.path.sep in requirement or "/" in requirement) and ( - cwd.joinpath(requirement).exists() - or Path(requirement).expanduser().exists() - and Path(requirement).expanduser().is_absolute() - ): - path = Path(requirement).expanduser() - is_absolute = path.is_absolute() - - if not path.is_absolute(): - path = cwd.joinpath(requirement) - - if path.is_file(): - package = Provider.get_package_from_file(path.resolve()) - else: - package = Provider.get_package_from_directory(path.resolve()) - - return { - "name": package.name, - "path": ( - path.relative_to(cwd).as_posix() if not is_absolute else path.as_posix() - ), - } - - return None - - -def _parse_dependency_specification_simple( - requirement: str, -) -> DependencySpec | None: - extras: list[str] = [] - pair = re.sub("^([^@=: ]+)(?:@|==|(?~!])=|:| )(.*)$", "\\1 \\2", requirement) - pair = pair.strip() - - require: DependencySpec = {} - - if " " in pair: - name, version = pair.split(" ", 1) - extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) - if extras_m: - extras = [e.strip() for e in extras_m.group(1).split(",")] - name, _ = name.split("[") - - require["name"] = name - if version != "latest": - require["version"] = version - else: - m = re.match(r"^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip()) - if m: - name, constraint = m.group(1), m.group(2) - extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) - if extras_m: - extras = [e.strip() for e in extras_m.group(1).split(",")] - name, _ = name.split("[") - - require["name"] = name - require["version"] = constraint - else: - extras_m = re.search(r"\[([\w\d,-_]+)\]$", pair) - if extras_m: - extras = [e.strip() for e in extras_m.group(1).split(",")] - pair, _ = pair.split("[") - - require["name"] = pair - - if extras: - require["extras"] = extras - - return require - - BaseSpec = TypeVar("BaseSpec", DependencySpec, InlineTable) @@ -178,49 +57,174 @@ def dependency_to_specification( return specification -def pep508_to_dependency_specification(requirement: str) -> DependencySpec | None: - if " ; " not in requirement and re.search(r"@[\^~!=<>\d]", requirement): - # this is of the form package@, do not attempt to parse it - return None +class RequirementsParser: + def __init__( + self, + *, + artifact_cache: ArtifactCache, + env: Env | None = None, + cwd: Path | None = None, + ) -> None: + self._direct_origin = DirectOrigin(artifact_cache) + self._env = env + self._cwd = cwd or Path.cwd() + + def parse(self, requirement: str) -> DependencySpec: + requirement = requirement.strip() - with contextlib.suppress(ValueError): - dependency = Dependency.create_from_pep_508(requirement) - specification: DependencySpec = {} - specification = dependency_to_specification(dependency, specification) + specification = self._parse_pep508(requirement) + + if specification is not None: + return specification + + extras = [] + extras_m = re.search(r"\[([\w\d,-_ ]+)\]$", requirement) + if extras_m: + extras = [e.strip() for e in extras_m.group(1).split(",")] + requirement, _ = requirement.split("[") + + specification = ( + self._parse_url(requirement) + or self._parse_path(requirement) + or self._parse_simple(requirement) + ) if specification: - specification["name"] = dependency.name + if extras and "extras" not in specification: + specification["extras"] = extras return specification - return None + raise ValueError(f"Invalid dependency specification: {requirement}") + + def _parse_pep508(self, requirement: str) -> DependencySpec | None: + if " ; " not in requirement and re.search(r"@[\^~!=<>\d]", requirement): + # this is of the form package@, do not attempt to parse it + return None + + with contextlib.suppress(ValueError): + dependency = Dependency.create_from_pep_508(requirement) + specification: DependencySpec = {} + specification = dependency_to_specification(dependency, specification) + + if specification: + specification["name"] = dependency.name + return specification + + return None + + def _parse_git_url(self, requirement: str) -> DependencySpec | None: + from poetry.core.vcs.git import Git + from poetry.core.vcs.git import ParsedUrl + + parsed = ParsedUrl.parse(requirement) + url = Git.normalize_url(requirement) + + pair = {"name": parsed.name, "git": url.url} + + if parsed.rev: + pair["rev"] = url.revision + if parsed.subdirectory: + pair["subdirectory"] = parsed.subdirectory -def parse_dependency_specification( - requirement: str, env: Env | None = None, cwd: Path | None = None -) -> DependencySpec: - requirement = requirement.strip() - cwd = cwd or Path.cwd() + source_root = self._env.path.joinpath("src") if self._env else None + package = self._direct_origin.get_package_from_vcs( + "git", + url=url.url, + rev=pair.get("rev"), + subdirectory=parsed.subdirectory, + source_root=source_root, + ) + pair["name"] = package.name + return pair - specification = pep508_to_dependency_specification(requirement) + def _parse_url(self, requirement: str) -> DependencySpec | None: + url_parsed = urllib.parse.urlparse(requirement) + if not (url_parsed.scheme and url_parsed.netloc): + return None - if specification is not None: - return specification + if url_parsed.scheme in ["git+https", "git+ssh"]: + return self._parse_git_url(requirement) - extras = [] - extras_m = re.search(r"\[([\w\d,-_ ]+)\]$", requirement) - if extras_m: - extras = [e.strip() for e in extras_m.group(1).split(",")] - requirement, _ = requirement.split("[") + if url_parsed.scheme in ["http", "https"]: + package = self._direct_origin.get_package_from_url(requirement) + assert package.source_url is not None + return {"name": package.name, "url": package.source_url} - specification = ( - _parse_dependency_specification_url(requirement, env=env) - or _parse_dependency_specification_path(requirement, cwd=cwd) - or _parse_dependency_specification_simple(requirement) - ) + return None + + def _parse_path(self, requirement: str) -> DependencySpec | None: + if (os.path.sep in requirement or "/" in requirement) and ( + self._cwd.joinpath(requirement).exists() + or Path(requirement).expanduser().exists() + and Path(requirement).expanduser().is_absolute() + ): + path = Path(requirement).expanduser() + is_absolute = path.is_absolute() + + if not path.is_absolute(): + path = self._cwd.joinpath(requirement) + + if path.is_file(): + package = self._direct_origin.get_package_from_file(path.resolve()) + else: + package = self._direct_origin.get_package_from_directory(path.resolve()) + + return { + "name": package.name, + "path": ( + path.relative_to(self._cwd).as_posix() + if not is_absolute + else path.as_posix() + ), + } + + return None - if specification: - if extras and "extras" not in specification: - specification["extras"] = extras - return specification + def _parse_simple( + self, + requirement: str, + ) -> DependencySpec | None: + extras: list[str] = [] + pair = re.sub( + "^([^@=: ]+)(?:@|==|(?~!])=|:| )(.*)$", "\\1 \\2", requirement + ) + pair = pair.strip() - raise ValueError(f"Invalid dependency specification: {requirement}") + require: DependencySpec = {} + + if " " in pair: + name, version = pair.split(" ", 1) + extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) + if extras_m: + extras = [e.strip() for e in extras_m.group(1).split(",")] + name, _ = name.split("[") + + require["name"] = name + if version != "latest": + require["version"] = version + else: + m = re.match( + r"^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip() + ) + if m: + name, constraint = m.group(1), m.group(2) + extras_m = re.search(r"\[([\w\d,-_]+)\]$", name) + if extras_m: + extras = [e.strip() for e in extras_m.group(1).split(",")] + name, _ = name.split("[") + + require["name"] = name + require["version"] = constraint + else: + extras_m = re.search(r"\[([\w\d,-_]+)\]$", pair) + if extras_m: + extras = [e.strip() for e in extras_m.group(1).split(",")] + pair, _ = pair.split("[") + + require["name"] = pair + + if extras: + require["extras"] = extras + + return require diff --git a/tests/conftest.py b/tests/conftest.py index 7e006d30be0..f1bc94187b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,7 @@ from poetry.layouts import layout from poetry.repositories import Repository from poetry.repositories import RepositoryPool +from poetry.utils.cache import ArtifactCache from poetry.utils.env import EnvManager from poetry.utils.env import SystemEnv from poetry.utils.env import VirtualEnv @@ -222,6 +223,11 @@ def config( return c +@pytest.fixture +def artifact_cache(config: Config) -> ArtifactCache: + return ArtifactCache(cache_dir=config.artifacts_cache_directory) + + @pytest.fixture() def config_dir(tmp_path: Path) -> Path: path = tmp_path / "config" @@ -239,7 +245,7 @@ def mock_user_config_dir(mocker: MockerFixture, config_dir: Path) -> None: def download_mock(mocker: MockerFixture) -> None: # Patch download to not download anything but to just copy from fixtures mocker.patch("poetry.utils.helpers.download_file", new=mock_download) - mocker.patch("poetry.puzzle.provider.download_file", new=mock_download) + mocker.patch("poetry.packages.direct_origin.download_file", new=mock_download) mocker.patch("poetry.repositories.http_repository.download_file", new=mock_download) diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py index ca00ba01e73..d134b90f9c0 100644 --- a/tests/installation/test_chef.py +++ b/tests/installation/test_chef.py @@ -14,7 +14,6 @@ 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 tests.repositories.test_pypi_repository import MockRepository @@ -22,6 +21,7 @@ if TYPE_CHECKING: from pytest_mock import MockerFixture + from poetry.utils.cache import ArtifactCache from tests.conftest import Config from tests.types import FixtureDirGetter @@ -40,11 +40,6 @@ def setup(mocker: MockerFixture, pool: RepositoryPool) -> None: mocker.patch.object(Factory, "create_pool", return_value=pool) -@pytest.fixture -def artifact_cache(config: Config) -> ArtifactCache: - return ArtifactCache(cache_dir=config.artifacts_cache_directory) - - def test_prepare_sdist( config: Config, config_cache_dir: Path, diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index 602e972b5db..0ec3653ebcf 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -135,11 +135,6 @@ def pool() -> RepositoryPool: return pool -@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 diff --git a/tests/packages/test_direct_origin.py b/tests/packages/test_direct_origin.py new file mode 100644 index 00000000000..ff9548c5fdf --- /dev/null +++ b/tests/packages/test_direct_origin.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import MagicMock + +from poetry.core.packages.utils.link import Link + +from poetry.packages.direct_origin import DirectOrigin +from poetry.utils.cache import ArtifactCache + + +if TYPE_CHECKING: + from pathlib import Path + + from pytest_mock import MockerFixture + + from tests.types import FixtureDirGetter + + +def test_direct_origin_get_package_from_file(fixture_dir: FixtureDirGetter) -> None: + wheel_path = fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl" + package = DirectOrigin.get_package_from_file(wheel_path) + assert package.name == "demo" + + +def test_direct_origin_caches_url_dependency(tmp_path: Path) -> None: + artifact_cache = ArtifactCache(cache_dir=tmp_path) + direct_origin = DirectOrigin(artifact_cache) + url = "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl" + + package = direct_origin.get_package_from_url(url) + + assert package.name == "demo" + assert artifact_cache.get_cached_archive_for_link(Link(url), strict=True) + + +def test_direct_origin_does_not_download_url_dependency_when_cached( + fixture_dir: FixtureDirGetter, mocker: MockerFixture +) -> None: + artifact_cache = MagicMock() + artifact_cache.get_cached_archive_for_link = MagicMock( + return_value=fixture_dir("distributions") / "demo-0.1.2-py2.py3-none-any.whl" + ) + direct_origin = DirectOrigin(artifact_cache) + url = "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl" + mocker.patch( + "poetry.packages.direct_origin.download_file", + side_effect=Exception("download_file should not be called"), + ) + + package = direct_origin.get_package_from_url(url) + + assert package.name == "demo" + artifact_cache.get_cached_archive_for_link.assert_called_once_with( + Link(url), strict=True + ) diff --git a/tests/test_factory.py b/tests/test_factory.py index f64aed75b09..0667c134227 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -543,7 +543,7 @@ def test_create_package_source_invalid( fixture_dir: FixtureDirGetter, ) -> None: with pytest.raises(InvalidSourceError) as e: - Factory.create_package_source(source, auth_config=config) + Factory.create_package_source(source, config=config) Factory().create_poetry(fixture_dir("with_source_pypi_url")) assert str(e.value) == expected diff --git a/tests/utils/test_dependency_specification.py b/tests/utils/test_dependency_specification.py index 8b1c1dfd1b1..a782a55924e 100644 --- a/tests/utils/test_dependency_specification.py +++ b/tests/utils/test_dependency_specification.py @@ -7,12 +7,13 @@ from deepdiff import DeepDiff -from poetry.utils.dependency_specification import parse_dependency_specification +from poetry.utils.dependency_specification import RequirementsParser if TYPE_CHECKING: from pytest_mock import MockerFixture + from poetry.utils.cache import ArtifactCache from poetry.utils.dependency_specification import DependencySpec @@ -104,7 +105,10 @@ ], ) def test_parse_dependency_specification( - requirement: str, specification: DependencySpec, mocker: MockerFixture + requirement: str, + specification: DependencySpec, + mocker: MockerFixture, + artifact_cache: ArtifactCache, ) -> None: original = Path.exists @@ -116,5 +120,7 @@ def _mock(self: Path) -> bool: mocker.patch("pathlib.Path.exists", _mock) assert not DeepDiff( - parse_dependency_specification(requirement), specification, ignore_order=True + RequirementsParser(artifact_cache=artifact_cache).parse(requirement), + specification, + ignore_order=True, )