From 2814922975050a0039df77808f9edc7f9507c139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sat, 16 Mar 2024 19:32:56 +0100 Subject: [PATCH] locker: lock transitive marker and groups for each package (#9427) --- src/poetry/installation/installer.py | 49 +- src/poetry/packages/locker.py | 34 +- .../packages/transitive_package_info.py | 15 + src/poetry/puzzle/provider.py | 1 + src/poetry/puzzle/solver.py | 202 +++++-- src/poetry/puzzle/transaction.py | 30 +- tests/console/commands/test_lock.py | 2 +- .../fixtures/extras-with-dependencies.test | 8 +- tests/installation/fixtures/extras.test | 6 +- .../installation/fixtures/install-no-dev.test | 5 +- .../fixtures/no-dependencies.test | 2 +- tests/installation/fixtures/remove.test | 3 +- .../fixtures/update-with-lock.test | 3 +- .../fixtures/update-with-locked-extras.test | 7 +- .../fixtures/with-conditional-dependency.test | 4 +- .../fixtures/with-dependencies-extras.test | 5 +- .../with-dependencies-nested-extras.test | 5 +- .../fixtures/with-dependencies.test | 4 +- ...irectory-dependency-poetry-transitive.test | 8 +- .../with-directory-dependency-poetry.test | 4 +- .../with-directory-dependency-setuptools.test | 5 +- .../with-duplicate-dependencies-update.test | 5 +- .../fixtures/with-duplicate-dependencies.test | 11 +- .../with-file-dependency-transitive.test | 6 +- .../fixtures/with-file-dependency.test | 4 +- .../fixtures/with-multiple-updates.test | 8 +- .../fixtures/with-optional-dependencies.test | 6 +- .../fixtures/with-platform-dependencies.test | 10 +- .../fixtures/with-prereleases.test | 4 +- .../fixtures/with-pypi-repository.test | 11 +- .../fixtures/with-python-versions.test | 5 +- .../with-same-version-url-dependencies.test | 8 +- .../fixtures/with-sub-dependencies.test | 6 +- .../fixtures/with-url-dependency.test | 4 +- .../with-vcs-dependency-with-extras.test | 5 +- .../with-vcs-dependency-without-ref.test | 4 +- ...ith-wheel-dependency-no-requires-dist.test | 3 +- tests/installation/test_installer.py | 20 +- tests/packages/test_locker.py | 264 ++++++++-- tests/puzzle/conftest.py | 31 ++ tests/puzzle/test_solver.py | 74 +-- tests/puzzle/test_solver_internals.py | 495 ++++++++++++++++++ tests/puzzle/test_transaction.py | 68 +-- 43 files changed, 1210 insertions(+), 244 deletions(-) create mode 100644 src/poetry/packages/transitive_package_info.py create mode 100644 tests/puzzle/test_solver_internals.py diff --git a/src/poetry/installation/installer.py b/src/poetry/installation/installer.py index 5dc1606863f..b918a153fac 100644 --- a/src/poetry/installation/installer.py +++ b/src/poetry/installation/installer.py @@ -7,8 +7,6 @@ from packaging.utils import canonicalize_name from poetry.installation.executor import Executor -from poetry.installation.operations import Uninstall -from poetry.installation.operations import Update from poetry.repositories import Repository from poetry.repositories import RepositoryPool from poetry.repositories.installed_repository import InstalledRepository @@ -20,12 +18,14 @@ from cleo.io.io import IO from packaging.utils import NormalizedName + from poetry.core.packages.package import Package from poetry.core.packages.path_dependency import PathDependency from poetry.core.packages.project_package import ProjectPackage from poetry.config.config import Config from poetry.installation.operations.operation import Operation from poetry.packages import Locker + from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.utils.env import Env @@ -196,12 +196,9 @@ def _do_refresh(self) -> int: with solver.provider.use_source_root( source_root=self._env.path.joinpath("src") ): - ops = solver.solve(use_latest=use_latest).calculate_operations() + solved_packages = solver.solve(use_latest=use_latest).get_solved_packages() - lockfile_repo = LockfileRepository() - self._populate_lockfile_repo(lockfile_repo, ops) - - self._write_lock_file(lockfile_repo, force=True) + self._write_lock_file(solved_packages, force=True) return 0 @@ -236,10 +233,18 @@ def _do_install(self) -> int: with solver.provider.use_source_root( source_root=self._env.path.joinpath("src") ): - ops = solver.solve(use_latest=self._whitelist).calculate_operations() + solution = solver.solve(use_latest=self._whitelist) + solved_packages = solution.get_solved_packages() + + if not self.executor.enabled: + # If we are only in lock mode, no need to go any further + self._write_lock_file(solved_packages) + return 0 lockfile_repo = LockfileRepository() - self._populate_lockfile_repo(lockfile_repo, ops) + for package in solved_packages: + if not lockfile_repo.has_package(package): + lockfile_repo.add_package(package) else: self._io.write_line("Installing dependencies from lock file") @@ -261,11 +266,6 @@ def _do_install(self) -> int: locked_repository = self._locker.locked_repository() lockfile_repo = locked_repository - if not self.executor.enabled: - # If we are only in lock mode, no need to go any further - self._write_lock_file(lockfile_repo) - return 0 - if self._io.is_verbose(): self._io.write_line("") self._io.write_line( @@ -312,13 +312,17 @@ def _do_install(self) -> int: if status == 0 and self._update: # Only write lock file when installation is success - self._write_lock_file(lockfile_repo) + self._write_lock_file(solved_packages) return status - def _write_lock_file(self, repo: LockfileRepository, force: bool = False) -> None: + def _write_lock_file( + self, + packages: dict[Package, TransitivePackageInfo], + force: bool = False, + ) -> None: if not self.is_dry_run() and (force or self._update): - updated_lock = self._locker.set_lock_data(self._package, repo.packages) + updated_lock = self._locker.set_lock_data(self._package, packages) if updated_lock: self._io.write_line("") @@ -327,16 +331,5 @@ def _write_lock_file(self, repo: LockfileRepository, force: bool = False) -> Non def _execute(self, operations: list[Operation]) -> int: return self._executor.execute(operations) - def _populate_lockfile_repo( - self, repo: LockfileRepository, ops: Iterable[Operation] - ) -> None: - for op in ops: - if isinstance(op, Uninstall): - continue - - package = op.target_package if isinstance(op, Update) else op.package - if not repo.has_package(package): - repo.add_package(package) - def _get_installed(self) -> InstalledRepository: return InstalledRepository.load(self._env) diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 420cd392d10..6e33be00498 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -38,6 +38,7 @@ from poetry.core.packages.vcs_dependency import VCSDependency from tomlkit.toml_document import TOMLDocument + from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.repositories.lockfile_repository import LockfileRepository logger = logging.getLogger(__name__) @@ -49,7 +50,7 @@ class Locker: - _VERSION = "2.0" + _VERSION = "2.1" _READ_VERSION_RANGE = ">=1,<3" _legacy_keys: ClassVar[list[str]] = [ @@ -231,7 +232,9 @@ def locked_repository(self) -> LockfileRepository: return repository - def set_lock_data(self, root: Package, packages: list[Package]) -> bool: + def set_lock_data( + self, root: Package, packages: dict[Package, TransitivePackageInfo] + ) -> bool: """Store lock data and eventually persist to the lock file""" lock = self._compute_lock_data(root, packages) @@ -242,7 +245,7 @@ def set_lock_data(self, root: Package, packages: list[Package]) -> bool: return False def _compute_lock_data( - self, root: Package, packages: list[Package] + self, root: Package, packages: dict[Package, TransitivePackageInfo] ) -> TOMLDocument: package_specs = self._lock_packages(packages) # Retrieving hashes @@ -356,7 +359,9 @@ def _get_lock_data(self) -> dict[str, Any]: return lock_data - def _lock_packages(self, packages: list[Package]) -> list[dict[str, Any]]: + def _lock_packages( + self, packages: dict[Package, TransitivePackageInfo] + ) -> list[dict[str, Any]]: locked = [] for package in sorted( @@ -371,13 +376,15 @@ def _lock_packages(self, packages: list[Package]) -> list[dict[str, Any]]: x.source_resolved_reference or "", ), ): - spec = self._dump_package(package) + spec = self._dump_package(package, packages[package]) locked.append(spec) return locked - def _dump_package(self, package: Package) -> dict[str, Any]: + def _dump_package( + self, package: Package, transitive_info: TransitivePackageInfo + ) -> dict[str, Any]: dependencies: dict[str, list[Any]] = {} for dependency in sorted( package.requires, @@ -447,8 +454,21 @@ def _dump_package(self, package: Package) -> dict[str, Any]: "description": package.description or "", "optional": package.optional, "python-versions": package.python_versions, - "files": sorted(package.files, key=lambda x: x["file"]), + "groups": sorted(transitive_info.groups, key=lambda x: (x != "main", x)), } + if transitive_info.markers: + if len(markers := set(transitive_info.markers.values())) == 1: + if not (marker := next(iter(markers))).is_any(): + data["markers"] = str(marker) + else: + data["markers"] = inline_table() + for k, v in sorted( + transitive_info.markers.items(), + key=lambda x: (x[0] != "main", x[0]), + ): + if not v.is_any(): + data["markers"][k] = str(v) + data["files"] = sorted(package.files, key=lambda x: x["file"]) if dependencies: data["dependencies"] = table() diff --git a/src/poetry/packages/transitive_package_info.py b/src/poetry/packages/transitive_package_info.py new file mode 100644 index 00000000000..84fb6e12d5d --- /dev/null +++ b/src/poetry/packages/transitive_package_info.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from poetry.core.version.markers import BaseMarker + + +@dataclass +class TransitivePackageInfo: + depth: int # max depth in the dependency tree + groups: set[str] + markers: dict[str, BaseMarker] # group -> marker diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index 754795b845c..17d28d3ba68 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -525,6 +525,7 @@ def complete_package( package = dependency_package.package dependency = dependency_package.dependency new_dependency = package.without_features().to_dependency() + new_dependency.marker = AnyMarker() # When adding dependency foo[extra] -> foo, preserve foo's source, if it's # specified. This prevents us from trying to get foo from PyPI diff --git a/src/poetry/puzzle/solver.py b/src/poetry/puzzle/solver.py index 9675bc1f6f0..2a72c2289d7 100644 --- a/src/poetry/puzzle/solver.py +++ b/src/poetry/puzzle/solver.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import time from collections import defaultdict @@ -9,8 +10,13 @@ from typing import Tuple from typing import TypeVar +from poetry.core.version.markers import AnyMarker +from poetry.core.version.markers import EmptyMarker +from poetry.core.version.markers import parse_marker + from poetry.mixology import resolve_version from poetry.mixology.failure import SolveFailure +from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.puzzle.exceptions import OverrideNeeded from poetry.puzzle.exceptions import SolverProblemError from poetry.puzzle.provider import Indicator @@ -24,9 +30,11 @@ from cleo.io.io import IO from packaging.utils import NormalizedName + from poetry.core.constraints.version import VersionConstraint from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage + from poetry.core.version.markers import BaseMarker from poetry.puzzle.transaction import Transaction from poetry.repositories import RepositoryPool @@ -69,7 +77,13 @@ def solve( with self._progress(), self._provider.use_latest_for(use_latest or []): start = time.time() - packages, depths = self._solve() + packages = self._solve() + # simplify markers by removing redundant information + for transitive_info in packages.values(): + for group, marker in transitive_info.markers.items(): + transitive_info.markers[group] = simplify_marker( + marker, self._package.python_constraint + ) end = time.time() if len(self._overrides) > 1: @@ -96,7 +110,7 @@ def solve( return Transaction( self._locked_packages, - list(zip(packages, depths)), + packages, installed_packages=self._installed_packages, root_package=self._package, ) @@ -120,9 +134,8 @@ def _progress(self) -> Iterator[None]: def _solve_in_compatibility_mode( self, overrides: tuple[dict[Package, dict[str, Dependency]], ...], - ) -> tuple[list[Package], list[int]]: - packages = [] - depths = [] + ) -> dict[Package, TransitivePackageInfo]: + packages: dict[Package, TransitivePackageInfo] = {} for override in overrides: self._provider.debug( # ignore the warning as provider does not do interpolation @@ -130,24 +143,12 @@ def _solve_in_compatibility_mode( f"with the following overrides ({override})." ) self._provider.set_overrides(override) - _packages, _depths = self._solve() - for index, package in enumerate(_packages): - if package not in packages: - packages.append(package) - depths.append(_depths[index]) - continue - else: - idx = packages.index(package) - pkg = packages[idx] - depths[idx] = max(depths[idx], _depths[index]) - - for dep in package.requires: - if dep not in pkg.requires: - pkg.add_dependency(dep) - - return packages, depths - - def _solve(self) -> tuple[list[Package], list[int]]: + new_packages = self._solve() + merge_packages_from_override(packages, new_packages, override) + + return packages + + def _solve(self) -> dict[Package, TransitivePackageInfo]: if self._provider._overrides: self._overrides.append(self._provider._overrides) @@ -160,12 +161,19 @@ def _solve(self) -> tuple[list[Package], list[int]]: except SolveFailure as e: raise SolverProblemError(e) - combined_nodes = depth_first_search(PackageNode(self._package, packages)) + return self._aggregate_solved_packages(packages) + + def _aggregate_solved_packages( + self, packages: list[Package] + ) -> dict[Package, TransitivePackageInfo]: + combined_nodes, markers = depth_first_search( + PackageNode(self._package, packages) + ) results = dict(aggregate_package_nodes(nodes) for nodes in combined_nodes) + calculate_markers(results, markers) # Merging feature packages with base packages - final_packages = [] - depths = [] + solved_packages = {} for package in packages: if package.features: for _package in packages: @@ -190,11 +198,9 @@ def _solve(self) -> tuple[list[Package], list[int]]: # because it includes relevant extras _dep.marker = dep.marker else: - final_packages.append(package) - depths.append(results[package]) + solved_packages[package] = results[package] - # Return the packages in their original order with associated depths - return final_packages, depths + return solved_packages DFSNodeID = Tuple[str, FrozenSet[str], bool] @@ -218,12 +224,15 @@ def __str__(self) -> str: return str(self.id) -def depth_first_search(source: PackageNode) -> list[list[PackageNode]]: +def depth_first_search( + source: PackageNode, +) -> tuple[list[list[PackageNode]], dict[Package, dict[Package, BaseMarker]]]: back_edges: dict[DFSNodeID, list[PackageNode]] = defaultdict(list) + markers: dict[Package, dict[Package, BaseMarker]] = defaultdict(dict) visited: set[DFSNodeID] = set() topo_sorted_nodes: list[PackageNode] = [] - dfs_visit(source, back_edges, visited, topo_sorted_nodes) + dfs_visit(source, back_edges, visited, topo_sorted_nodes, markers) # Combine the nodes by name combined_nodes: dict[str, list[PackageNode]] = defaultdict(list) @@ -237,7 +246,7 @@ def depth_first_search(source: PackageNode) -> list[list[PackageNode]]: if node.name in combined_nodes ] - return combined_topo_sorted_nodes + return combined_topo_sorted_nodes, markers def dfs_visit( @@ -245,14 +254,20 @@ def dfs_visit( back_edges: dict[DFSNodeID, list[PackageNode]], visited: set[DFSNodeID], sorted_nodes: list[PackageNode], + markers: dict[Package, dict[Package, BaseMarker]], ) -> None: if node.id in visited: return visited.add(node.id) - for neighbor in node.reachable(): - back_edges[neighbor.id].append(node) - dfs_visit(neighbor, back_edges, visited, sorted_nodes) + for out_neighbor in node.reachable(): + back_edges[out_neighbor.id].append(node) + markers[out_neighbor.package][node.package] = ( + out_neighbor.marker + if node.package.is_root() + else out_neighbor.marker.without_extras() + ) + dfs_visit(out_neighbor, back_edges, visited, sorted_nodes, markers) sorted_nodes.insert(0, node) @@ -263,11 +278,13 @@ def __init__( packages: list[Package], previous: PackageNode | None = None, dep: Dependency | None = None, + marker: BaseMarker | None = None, ) -> None: self.package = package self.packages = packages self.dep = dep + self.marker = marker or AnyMarker() self.depth = -1 if not previous: @@ -279,9 +296,10 @@ def __init__( else: raise ValueError("Both previous and dep must be passed") + package_repr = repr(package) super().__init__( - (package.complete_name, self.groups, self.optional), - package.complete_name, + (package_repr, self.groups, self.optional), + package_repr, package.name, ) @@ -290,15 +308,26 @@ def reachable(self) -> Sequence[PackageNode]: for dependency in self.package.all_requires: for pkg in self.packages: - if pkg.complete_name == dependency.complete_name and ( - dependency.constraint.allows(pkg.version) + if pkg.complete_name == dependency.complete_name and pkg.satisfies( + dependency ): + marker = dependency.marker + if self.package.is_root() and dependency.in_extras: + marker = marker.intersect( + parse_marker( + " or ".join( + f'extra == "{extra}"' + for extra in dependency.in_extras + ) + ) + ) children.append( PackageNode( pkg, self.packages, self, self.dep or dependency, + marker, ) ) @@ -316,12 +345,14 @@ def visit(self, parents: list[PackageNode]) -> None: ) -def aggregate_package_nodes(nodes: list[PackageNode]) -> tuple[Package, int]: +def aggregate_package_nodes( + nodes: list[PackageNode], +) -> tuple[Package, TransitivePackageInfo]: package = nodes[0].package depth = max(node.depth for node in nodes) - groups: list[str] = [] + groups: set[str] = set() for node in nodes: - groups.extend(node.groups) + groups.update(node.groups) optional = all(node.optional for node in nodes) for node in nodes: @@ -330,4 +361,87 @@ def aggregate_package_nodes(nodes: list[PackageNode]) -> tuple[Package, int]: package.optional = optional - return package, depth + # TransitivePackageInfo.markers is updated later, + # because the nodes of all packages have to be aggregated first. + return package, TransitivePackageInfo(depth, groups, {}) + + +def calculate_markers( + packages: dict[Package, TransitivePackageInfo], + markers: dict[Package, dict[Package, BaseMarker]], +) -> None: + # group packages by depth + packages_by_depth: dict[int, list[Package]] = defaultdict(list) + max_depth = -1 + for package, info in packages.items(): + max_depth = max(max_depth, info.depth) + packages_by_depth[info.depth].append(package) + + # calculate markers from lowest to highest depth + # (start with depth 0 because the root package has depth -1) + has_incomplete_markers = True + while has_incomplete_markers: + has_incomplete_markers = False + for depth in range(max_depth + 1): + for package in packages_by_depth[depth]: + transitive_info = packages[package] + transitive_marker: dict[str, BaseMarker] = { + group: EmptyMarker() for group in transitive_info.groups + } + for parent, m in markers[package].items(): + parent_info = packages[parent] + if parent_info.groups: + if parent_info.groups != set(parent_info.markers): + # there is a cycle -> we need one more iteration + has_incomplete_markers = True + continue + for group in parent_info.groups: + transitive_marker[group] = transitive_marker[group].union( + parent_info.markers[group].intersect(m) + ) + else: + for group in transitive_info.groups: + transitive_marker[group] = transitive_marker[group].union(m) + transitive_info.markers = transitive_marker + + +def merge_packages_from_override( + packages: dict[Package, TransitivePackageInfo], + new_packages: dict[Package, TransitivePackageInfo], + override: dict[Package, dict[str, Dependency]], +) -> None: + override_marker: BaseMarker = AnyMarker() + for deps in override.values(): + for dep in deps.values(): + override_marker = override_marker.intersect(dep.marker.without_extras()) + for new_package, new_package_info in new_packages.items(): + if package_info := packages.get(new_package): + # update existing package + package_info.depth = max(package_info.depth, new_package_info.depth) + package_info.groups.update(new_package_info.groups) + for group, marker in new_package_info.markers.items(): + package_info.markers[group] = package_info.markers.get( + group, EmptyMarker() + ).union(override_marker.intersect(marker)) + for package in packages: + if package == new_package: + for dep in new_package.requires: + if dep not in package.requires: + package.add_dependency(dep) + + else: + for group, marker in new_package_info.markers.items(): + new_package_info.markers[group] = override_marker.intersect(marker) + packages[new_package] = new_package_info + + +@functools.lru_cache(maxsize=None) +def simplify_marker( + marker: BaseMarker, python_constraint: VersionConstraint +) -> BaseMarker: + """ + Remove constraints from markers that are covered by the projects Python constraint. + + Use cache because we call this function often for the same markers. + """ + return marker.reduce_by_python_constraint(python_constraint) diff --git a/src/poetry/puzzle/transaction.py b/src/poetry/puzzle/transaction.py index 13d964f3876..6fedc491315 100644 --- a/src/poetry/puzzle/transaction.py +++ b/src/poetry/puzzle/transaction.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import defaultdict from typing import TYPE_CHECKING from poetry.utils.extras import get_extra_package_names @@ -10,13 +11,14 @@ from poetry.core.packages.package import Package from poetry.installation.operations.operation import Operation + from poetry.packages.transitive_package_info import TransitivePackageInfo class Transaction: def __init__( self, current_packages: list[Package], - result_packages: list[tuple[Package, int]], + result_packages: list[Package] | dict[Package, TransitivePackageInfo], installed_packages: list[Package] | None = None, root_package: Package | None = None, ) -> None: @@ -29,6 +31,10 @@ def __init__( self._installed_packages = installed_packages self._root_package = root_package + def get_solved_packages(self) -> dict[Package, TransitivePackageInfo]: + assert isinstance(self._result_packages, dict) + return self._result_packages + def calculate_operations( self, with_uninstalls: bool = True, @@ -47,13 +53,19 @@ def calculate_operations( if extras is not None: assert self._root_package is not None extra_packages = get_extra_package_names( - [package for package, _ in self._result_packages], + self._result_packages, {k: [d.name for d in v] for k, v in self._root_package.extras.items()}, extras, ) + if isinstance(self._result_packages, dict): + priorities = { + pkg: info.depth for pkg, info in self._result_packages.items() + } + else: + priorities = defaultdict(int) uninstalls: set[NormalizedName] = set() - for result_package, priority in self._result_packages: + for result_package in self._result_packages: installed = False is_unsolicited_extra = extras is not None and ( result_package.optional and result_package.name not in extra_packages @@ -87,7 +99,11 @@ def calculate_operations( and not result_package.is_same_package_as(installed_package) ): operations.append( - Update(installed_package, result_package, priority=priority) + Update( + installed_package, + result_package, + priority=priorities[result_package], + ) ) else: operations.append( @@ -100,7 +116,7 @@ def calculate_operations( installed or (skip_directory and result_package.source_type == "directory") ): - op = Install(result_package, priority=priority) + op = Install(result_package, priority=priorities[result_package]) if is_unsolicited_extra: op.skip("Not required") operations.append(op) @@ -109,7 +125,7 @@ def calculate_operations( for current_package in self._current_packages: found = any( current_package.name == result_package.name - for result_package, _ in self._result_packages + for result_package in self._result_packages ) if not found: @@ -120,7 +136,7 @@ def calculate_operations( if synchronize: result_package_names = { - result_package.name for result_package, _ in self._result_packages + result_package.name for result_package in self._result_packages } # We preserve pip when not managed by poetry, this is done to avoid # externally managed virtual environments causing unnecessary removals. diff --git a/tests/console/commands/test_lock.py b/tests/console/commands/test_lock.py index 38d0c746412..edfa5f0ec11 100644 --- a/tests/console/commands/test_lock.py +++ b/tests/console/commands/test_lock.py @@ -166,7 +166,7 @@ def test_lock_no_update( assert len(packages) == len(locked_repository.packages) - assert locker.lock_data["metadata"].get("lock-version") == "2.0" + assert locker.lock_data["metadata"].get("lock-version") == "2.1" for package in packages: assert locked_repository.find_packages(package.to_dependency()) diff --git a/tests/installation/fixtures/extras-with-dependencies.test b/tests/installation/fixtures/extras-with-dependencies.test index 2dc9f2bafb3..d1d90ecde8e 100644 --- a/tests/installation/fixtures/extras-with-dependencies.test +++ b/tests/installation/fixtures/extras-with-dependencies.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -12,6 +13,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -20,6 +22,8 @@ version = "1.0" description = "" optional = true python-versions = "*" +groups = ["main"] +markers = 'extra == "foo"' files = [] [package.dependencies] @@ -31,6 +35,8 @@ version = "1.1" description = "" optional = true python-versions = "*" +groups = ["main"] +markers = 'extra == "foo"' files = [] [extras] @@ -38,5 +44,5 @@ foo = ["C"] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/extras.test b/tests/installation/fixtures/extras.test index e3b4b1653a3..09fef493e78 100644 --- a/tests/installation/fixtures/extras.test +++ b/tests/installation/fixtures/extras.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -12,6 +13,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -20,6 +22,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -28,6 +31,7 @@ version = "1.1" description = "" optional = true python-versions = "*" +groups = ["main"] files = [] [extras] @@ -35,5 +39,5 @@ foo = ["D"] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/install-no-dev.test b/tests/installation/fixtures/install-no-dev.test index d874343c155..6b9d864e4b9 100644 --- a/tests/installation/fixtures/install-no-dev.test +++ b/tests/installation/fixtures/install-no-dev.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -12,6 +13,7 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -20,9 +22,10 @@ version = "1.2" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/no-dependencies.test b/tests/installation/fixtures/no-dependencies.test index 374f79aae8a..29233434af8 100644 --- a/tests/installation/fixtures/no-dependencies.test +++ b/tests/installation/fixtures/no-dependencies.test @@ -2,5 +2,5 @@ package = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/remove.test b/tests/installation/fixtures/remove.test index 0a264b66e42..33727ef94a2 100644 --- a/tests/installation/fixtures/remove.test +++ b/tests/installation/fixtures/remove.test @@ -4,9 +4,10 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/update-with-lock.test b/tests/installation/fixtures/update-with-lock.test index eafd48836c0..1cb00f344d5 100644 --- a/tests/installation/fixtures/update-with-lock.test +++ b/tests/installation/fixtures/update-with-lock.test @@ -4,9 +4,10 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/update-with-locked-extras.test b/tests/installation/fixtures/update-with-locked-extras.test index 348621aa9a3..e70f409c56e 100644 --- a/tests/installation/fixtures/update-with-locked-extras.test +++ b/tests/installation/fixtures/update-with-locked-extras.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -19,6 +20,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -27,6 +29,8 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'python_version < "2.8"' files = [] [[package]] @@ -35,9 +39,10 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-conditional-dependency.test b/tests/installation/fixtures/with-conditional-dependency.test index 090bee4025e..8f8b35907c2 100644 --- a/tests/installation/fixtures/with-conditional-dependency.test +++ b/tests/installation/fixtures/with-conditional-dependency.test @@ -4,6 +4,7 @@ version = "1.0.0" description = "" optional = false python-versions = ">=3.5" +groups = ["main"] files = [] [package.requirements] @@ -15,6 +16,7 @@ version = "1.0.1" description = "" optional = false python-versions = ">=3.6" +groups = ["main"] files = [] [package.requirements] @@ -22,5 +24,5 @@ python = ">=3.6,<4.0" [metadata] python-versions = "~2.7 || ^3.4" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-dependencies-extras.test b/tests/installation/fixtures/with-dependencies-extras.test index 486e43fbc32..83360848d77 100644 --- a/tests/installation/fixtures/with-dependencies-extras.test +++ b/tests/installation/fixtures/with-dependencies-extras.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -12,6 +13,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -26,9 +28,10 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-dependencies-nested-extras.test b/tests/installation/fixtures/with-dependencies-nested-extras.test index 6b07ddb4271..49b3cf0b991 100644 --- a/tests/installation/fixtures/with-dependencies-nested-extras.test +++ b/tests/installation/fixtures/with-dependencies-nested-extras.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -18,6 +19,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -32,9 +34,10 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-dependencies.test b/tests/installation/fixtures/with-dependencies.test index aa74c20c8f7..05c6162ebc2 100644 --- a/tests/installation/fixtures/with-dependencies.test +++ b/tests/installation/fixtures/with-dependencies.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -12,9 +13,10 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-directory-dependency-poetry-transitive.test b/tests/installation/fixtures/with-directory-dependency-poetry-transitive.test index 4459d737602..58fd430d372 100644 --- a/tests/installation/fixtures/with-directory-dependency-poetry-transitive.test +++ b/tests/installation/fixtures/with-directory-dependency-poetry-transitive.test @@ -4,6 +4,7 @@ name = "demo" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.1.0" +groups = ["main"] [[package.files]] file = "demo-0.1.0-py2.py3-none-any.whl" @@ -26,6 +27,7 @@ develop = false name = "inner-directory-project" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.2.4" @@ -38,6 +40,7 @@ description = "" name = "pendulum" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.4.4" @@ -47,6 +50,7 @@ develop = false name = "project-with-extras" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.2.3" @@ -64,6 +68,7 @@ develop = false name = "project-with-transitive-directory-dependencies" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.2.3" @@ -81,6 +86,7 @@ develop = false name = "project-with-transitive-file-dependencies" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.2.3" @@ -94,5 +100,5 @@ url = "project_with_transitive_file_dependencies" [metadata] content-hash = "123456789" -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" diff --git a/tests/installation/fixtures/with-directory-dependency-poetry.test b/tests/installation/fixtures/with-directory-dependency-poetry.test index 312970eeea9..edb86a3afd4 100644 --- a/tests/installation/fixtures/with-directory-dependency-poetry.test +++ b/tests/installation/fixtures/with-directory-dependency-poetry.test @@ -3,6 +3,7 @@ description = "" name = "pendulum" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.4.4" @@ -12,6 +13,7 @@ develop = false name = "project-with-extras" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.2.3" @@ -28,5 +30,5 @@ url = "tests/fixtures/project_with_extras" [metadata] content-hash = "123456789" -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" diff --git a/tests/installation/fixtures/with-directory-dependency-setuptools.test b/tests/installation/fixtures/with-directory-dependency-setuptools.test index ed396a7cdc3..ff14323ff38 100644 --- a/tests/installation/fixtures/with-directory-dependency-setuptools.test +++ b/tests/installation/fixtures/with-directory-dependency-setuptools.test @@ -4,6 +4,7 @@ version = "0.2.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -12,6 +13,7 @@ version = "1.4.4" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -21,6 +23,7 @@ develop = false description = "Demo project." optional = false python-versions = "*" +groups = ["main"] files = [] [package.source] @@ -33,5 +36,5 @@ pendulum = ">=1.4.4" [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-duplicate-dependencies-update.test b/tests/installation/fixtures/with-duplicate-dependencies-update.test index d68fe893918..e552f742c33 100644 --- a/tests/installation/fixtures/with-duplicate-dependencies-update.test +++ b/tests/installation/fixtures/with-duplicate-dependencies-update.test @@ -4,6 +4,7 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -15,6 +16,7 @@ version = "2.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -26,9 +28,10 @@ version = "1.5" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-duplicate-dependencies.test b/tests/installation/fixtures/with-duplicate-dependencies.test index 3c88e7c18f8..b7f3bfeb556 100644 --- a/tests/installation/fixtures/with-duplicate-dependencies.test +++ b/tests/installation/fixtures/with-duplicate-dependencies.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -18,6 +19,8 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'python_version < "4.0"' files = [] [package.dependencies] @@ -29,6 +32,8 @@ version = "2.0" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'python_version >= "4.0"' files = [] [package.dependencies] @@ -40,6 +45,8 @@ version = "1.2" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'python_version < "4.0"' files = [] [[package]] @@ -48,9 +55,11 @@ version = "1.5" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'python_version >= "4.0"' files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-file-dependency-transitive.test b/tests/installation/fixtures/with-file-dependency-transitive.test index 738c388e779..d4ead66c4ea 100644 --- a/tests/installation/fixtures/with-file-dependency-transitive.test +++ b/tests/installation/fixtures/with-file-dependency-transitive.test @@ -4,6 +4,7 @@ name = "demo" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.1.0" +groups = ["main"] [[package.files]] file = "demo-0.1.0-py2.py3-none-any.whl" @@ -26,6 +27,7 @@ develop = false name = "inner-directory-project" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.2.4" @@ -38,6 +40,7 @@ description = "" name = "pendulum" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.4.4" @@ -47,6 +50,7 @@ develop = false name = "project-with-transitive-file-dependencies" optional = false python-versions = "*" +groups = ["main"] files = [] version = "1.2.3" @@ -60,5 +64,5 @@ url = "project_with_transitive_file_dependencies" [metadata] content-hash = "123456789" -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" diff --git a/tests/installation/fixtures/with-file-dependency.test b/tests/installation/fixtures/with-file-dependency.test index e5be6578903..6ac4b4b8b45 100644 --- a/tests/installation/fixtures/with-file-dependency.test +++ b/tests/installation/fixtures/with-file-dependency.test @@ -4,6 +4,7 @@ version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] [[package.files]] file = "demo-0.1.0-py2.py3-none-any.whl" @@ -26,9 +27,10 @@ version = "1.4.4" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-multiple-updates.test b/tests/installation/fixtures/with-multiple-updates.test index 4c6e401ad97..28c270323f2 100644 --- a/tests/installation/fixtures/with-multiple-updates.test +++ b/tests/installation/fixtures/with-multiple-updates.test @@ -4,6 +4,7 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -19,6 +20,7 @@ version = "1.1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -27,6 +29,8 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'python_version < "2.8"' files = [] [[package]] @@ -35,9 +39,11 @@ version = "2.0" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'python_version >= "3.4"' files = [] [metadata] python-versions = "~2.7 || ^3.4" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-optional-dependencies.test b/tests/installation/fixtures/with-optional-dependencies.test index 9d6c85f70f9..92bdff9ce68 100644 --- a/tests/installation/fixtures/with-optional-dependencies.test +++ b/tests/installation/fixtures/with-optional-dependencies.test @@ -4,6 +4,8 @@ version = "1.0" description = "" optional = true python-versions = "*" +groups = ["main"] +markers = 'extra == "foo"' files = [] [[package]] @@ -12,6 +14,7 @@ version = "1.3" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -23,6 +26,7 @@ version = "1.4" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [extras] @@ -30,5 +34,5 @@ foo = ["A"] [metadata] python-versions = "~2.7 || ^3.4" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-platform-dependencies.test b/tests/installation/fixtures/with-platform-dependencies.test index 4fc99d9183b..dc6659e4f96 100644 --- a/tests/installation/fixtures/with-platform-dependencies.test +++ b/tests/installation/fixtures/with-platform-dependencies.test @@ -4,6 +4,8 @@ version = "1.0" description = "" optional = true python-versions = "*" +groups = ["main"] +markers = 'extra == "foo"' files = [] [[package]] @@ -12,6 +14,8 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'sys_platform == "custom"' files = [] [[package]] @@ -20,6 +24,8 @@ version = "1.3" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'sys_platform == "darwin"' files = [] [package.dependencies] @@ -31,6 +37,8 @@ version = "1.4" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'sys_platform == "darwin"' files = [] [extras] @@ -38,5 +46,5 @@ foo = ["A"] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-prereleases.test b/tests/installation/fixtures/with-prereleases.test index 19347ae9364..b588a09eb35 100644 --- a/tests/installation/fixtures/with-prereleases.test +++ b/tests/installation/fixtures/with-prereleases.test @@ -4,6 +4,7 @@ version = "1.0a2" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -12,9 +13,10 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-pypi-repository.test b/tests/installation/fixtures/with-pypi-repository.test index d506a21ccbd..f277d537dbd 100644 --- a/tests/installation/fixtures/with-pypi-repository.test +++ b/tests/installation/fixtures/with-pypi-repository.test @@ -6,6 +6,7 @@ version = "17.4.0" description = "Classes Without Boilerplate" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "attrs-17.4.0-py2.py3-none-any.whl", hash = "sha256:d38e57f381e891928357c68e300d28d3d4dcddc50486d5f8dfaf743d40477619"}, {file = "attrs-17.4.0.tar.gz", hash = "sha256:eb7536a1e6928190b3008c5b350bdf9850d619fff212341cd096f87a27a5e564"}, @@ -22,6 +23,8 @@ version = "0.3.9" description = "Cross-platform colored terminal text." optional = false python-versions = "*" +groups = ["dev"] +markers = 'sys_platform == "win32"' files = [ {file = "colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:5b632359f1ed2b7676a869812ba0edaacb99be04679b29eb56c07a5e137ab5a2"}, {file = "colorama-0.3.9.tar.gz", hash = "sha256:4c5a15209723ce1330a5c193465fe221098f761e9640d823a2ce7c03f983137f"}, @@ -33,6 +36,7 @@ version = "4.1.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "more-itertools-4.1.0.tar.gz", hash = "sha256:bab2dc6f4be8f9a4a72177842c5283e2dff57c167439a03e3d8d901e854f0f2e"}, {file = "more_itertools-4.1.0-py2-none-any.whl", hash = "sha256:5dd7dfd88d2fdaea446da478ffef8d7151fdf26ee92ac7ed7b14e8d71efe4b62"}, @@ -48,6 +52,7 @@ version = "0.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "pluggy-0.6.0-py2-none-any.whl", hash = "sha256:9b835f86bfe5498c87ace7f4899cb1b0c40e71c9277377f6851c74a307879285"}, {file = "pluggy-0.6.0-py3-none-any.whl", hash = "sha256:8c646771f5eab7557d1f3924077c55408e86bdfb700f7d86a6d83abeabff4c66"}, @@ -60,6 +65,7 @@ version = "1.5.3" description = "library with cross-python path, ini-parsing, io, code, log facilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "py-1.5.3-py2.py3-none-any.whl", hash = "sha256:43ee6c7f95e0ec6a906de49906b79d138d89728fff17109d49f086abc2fdd985"}, {file = "py-1.5.3.tar.gz", hash = "sha256:2df2c513c3af11de15f58189ba5539ddc4768c6f33816dc5c03950c8bd6180fa"}, @@ -71,6 +77,7 @@ version = "3.5.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "pytest-3.5.1-py2.py3-none-any.whl", hash = "sha256:6d3e83b1c1697d220137e436980e73b3ca674f643e666d7c24b0321cb57b76a4"}, {file = "pytest-3.5.1.tar.gz", hash = "sha256:b8fe151f3e181801dd38583a1c03818fbc662a8fce96c9063a0af624613e78f8"}, @@ -91,6 +98,7 @@ version = "67.6.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, {file = "setuptools-67.6.1.tar.gz", hash = "sha256:a737d365c957dd3fced9ddd246118e95dce7a62c3dc49f37e7fdd9e93475d785"}, @@ -107,12 +115,13 @@ version = "1.11.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "six-1.11.0-py2.py3-none-any.whl", hash = "sha256:112f5b46e6aa106db3e4e2494a03694c938f41c4c4535edbdfc816c2e0cb50f2"}, {file = "six-1.11.0.tar.gz", hash = "sha256:268a4ccb159c1a2d2c79336b02e75058387b0cdbb4cea2f07846a758f48a356d"}, ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.7" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-python-versions.test b/tests/installation/fixtures/with-python-versions.test index 6c39d957e88..9cbcc32ef82 100644 --- a/tests/installation/fixtures/with-python-versions.test +++ b/tests/installation/fixtures/with-python-versions.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -12,6 +13,7 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -20,9 +22,10 @@ version = "1.2" description = "" optional = false python-versions = "~2.7 || ^3.3" +groups = ["main"] files = [] [metadata] python-versions = "~2.7 || ^3.4" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-same-version-url-dependencies.test b/tests/installation/fixtures/with-same-version-url-dependencies.test index 2188ee95a46..053f9cbdc25 100644 --- a/tests/installation/fixtures/with-same-version-url-dependencies.test +++ b/tests/installation/fixtures/with-same-version-url-dependencies.test @@ -4,6 +4,8 @@ version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = 'sys_platform == "win32"' files = [ {file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"} ] @@ -25,6 +27,8 @@ version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = 'sys_platform == "linux"' files = [ {file = "demo-0.1.0.tar.gz", hash = "sha256:9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad"} ] @@ -45,9 +49,11 @@ version = "1.4.4" description = "" optional = false python-versions = "*" +groups = ["main"] +markers = 'sys_platform == "linux" or sys_platform == "win32"' files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-sub-dependencies.test b/tests/installation/fixtures/with-sub-dependencies.test index 03ece3f5b62..cf4431837a5 100644 --- a/tests/installation/fixtures/with-sub-dependencies.test +++ b/tests/installation/fixtures/with-sub-dependencies.test @@ -4,6 +4,7 @@ version = "1.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -15,6 +16,7 @@ version = "1.1" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -26,6 +28,7 @@ version = "1.2" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -34,9 +37,10 @@ version = "1.3" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-url-dependency.test b/tests/installation/fixtures/with-url-dependency.test index ec40b26055a..be499e7157f 100644 --- a/tests/installation/fixtures/with-url-dependency.test +++ b/tests/installation/fixtures/with-url-dependency.test @@ -4,6 +4,7 @@ version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] files = [ {file = "demo-0.1.0-py2.py3-none-any.whl", hash = "sha256:70e704135718fffbcbf61ed1fc45933cfd86951a744b681000eaaa75da31f17a"} ] @@ -25,9 +26,10 @@ version = "1.4.4" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-vcs-dependency-with-extras.test b/tests/installation/fixtures/with-vcs-dependency-with-extras.test index aead6aabc1f..b3d2eaf2aba 100644 --- a/tests/installation/fixtures/with-vcs-dependency-with-extras.test +++ b/tests/installation/fixtures/with-vcs-dependency-with-extras.test @@ -4,6 +4,7 @@ version = "0.1.2" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] develop = false @@ -26,6 +27,7 @@ version = "1.4.4" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -34,9 +36,10 @@ version = "1.0.0" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-vcs-dependency-without-ref.test b/tests/installation/fixtures/with-vcs-dependency-without-ref.test index 31fecec2c78..d368e20eba6 100644 --- a/tests/installation/fixtures/with-vcs-dependency-without-ref.test +++ b/tests/installation/fixtures/with-vcs-dependency-without-ref.test @@ -4,6 +4,7 @@ version = "0.1.2" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] develop = false @@ -22,9 +23,10 @@ version = "1.4.4" description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/fixtures/with-wheel-dependency-no-requires-dist.test b/tests/installation/fixtures/with-wheel-dependency-no-requires-dist.test index a7bc27e596c..a02d7fbd8fa 100644 --- a/tests/installation/fixtures/with-wheel-dependency-no-requires-dist.test +++ b/tests/installation/fixtures/with-wheel-dependency-no-requires-dist.test @@ -4,6 +4,7 @@ version = "0.1.0" description = "" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] [[package.files]] file = "demo-0.1.0-py2.py3-none-any.whl" @@ -15,5 +16,5 @@ url = "tests/fixtures/wheel_with_no_requires_dist/demo-0.1.0-py2.py3-none-any.wh [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 681e882b19c..07b05b84adf 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -894,9 +894,9 @@ def test_run_with_optional_and_python_restricted_dependencies( repo.add_package(package_d) package.extras = {canonicalize_name("foo"): [get_dependency("A", "~1.0")]} - package.add_dependency( - Factory.create_dependency("A", {"version": "~1.0", "optional": True}) - ) + dep_a = Factory.create_dependency("A", {"version": "~1.0", "optional": True}) + dep_a._in_extras = [canonicalize_name("foo")] + package.add_dependency(dep_a) package.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "python": "~2.4"}) ) @@ -942,9 +942,9 @@ def test_run_with_optional_and_platform_restricted_dependencies( repo.add_package(package_d) package.extras = {canonicalize_name("foo"): [get_dependency("A", "~1.0")]} - package.add_dependency( - Factory.create_dependency("A", {"version": "~1.0", "optional": True}) - ) + dep_a = Factory.create_dependency("A", {"version": "~1.0", "optional": True}) + dep_a._in_extras = [canonicalize_name("foo")] + package.add_dependency(dep_a) package.add_dependency( Factory.create_dependency("B", {"version": "^1.0", "platform": "custom"}) ) @@ -1055,9 +1055,9 @@ def test_run_installs_extras_with_deps_if_requested( package.add_dependency(Factory.create_dependency("A", "^1.0")) package.add_dependency(Factory.create_dependency("B", "^1.0")) - package.add_dependency( - Factory.create_dependency("C", {"version": "^1.0", "optional": True}) - ) + dep_c = Factory.create_dependency("C", {"version": "^1.0", "optional": True}) + dep_c._in_extras = [canonicalize_name("foo")] + package.add_dependency(dep_c) package_c.add_dependency(Factory.create_dependency("D", "^1.0")) @@ -1116,7 +1116,7 @@ def test_installer_with_pypi_repository( assert result == 0 expected = fixture("with-pypi-repository") - assert expected == locker.written_data + assert locker.written_data == expected def test_run_installs_with_local_file( diff --git a/tests/packages/test_locker.py b/tests/packages/test_locker.py index fd11eab8dec..c62f98fe87a 100644 --- a/tests/packages/test_locker.py +++ b/tests/packages/test_locker.py @@ -15,13 +15,17 @@ from packaging.utils import canonicalize_name from poetry.core.constraints.version import Version +from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.package import Package from poetry.core.packages.project_package import ProjectPackage +from poetry.core.version.markers import AnyMarker +from poetry.core.version.markers import parse_marker from poetry.__version__ import __version__ from poetry.factory import Factory from poetry.packages.locker import GENERATED_COMMENT from poetry.packages.locker import Locker +from poetry.packages.transitive_package_info import TransitivePackageInfo from tests.helpers import get_dependency from tests.helpers import get_package @@ -45,7 +49,14 @@ def root() -> ProjectPackage: return ProjectPackage("root", "1.2.3") -def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None: +@pytest.fixture +def transitive_info() -> TransitivePackageInfo: + return TransitivePackageInfo(0, {MAIN_GROUP}, {}) + + +def test_lock_file_data_is_ordered( + locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo +) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0")) package_a.files = [{"file": "foo", "hash": "456"}, {"file": "bar", "hash": "123"}] @@ -80,15 +91,15 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None source_type="url", source_url="https://example.org/url-package-1.0-cp39-win_amd64.whl", ) - packages = [ - package_a2, - package_a, - get_package("B", "1.2"), - package_git, - package_git_with_subdirectory, - package_url_win32, - package_url_linux, - ] + packages = { + package_a2: transitive_info, + package_a: transitive_info, + get_package("B", "1.2"): transitive_info, + package_git: transitive_info, + package_git_with_subdirectory: transitive_info, + package_url_win32: transitive_info, + package_url_linux: transitive_info, + } locker.set_lock_data(root, packages) @@ -104,6 +115,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None description = "" optional = false python-versions = "*" +groups = ["main"] files = [ {{file = "bar", hash = "123"}}, {{file = "foo", hash = "456"}}, @@ -118,6 +130,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None description = "" optional = false python-versions = "*" +groups = ["main"] files = [ {{file = "baz", hash = "345"}}, ] @@ -128,6 +141,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [[package]] @@ -136,6 +150,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None description = "" optional = false python-versions = "*" +groups = ["main"] files = [] develop = false @@ -151,6 +166,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None description = "" optional = false python-versions = "*" +groups = ["main"] files = [] develop = false @@ -167,6 +183,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.source] @@ -179,6 +196,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.source] @@ -186,7 +204,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage) -> None url = "https://example.org/url-package-1.0-cp39-win_amd64.whl" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -204,6 +222,7 @@ def test_locker_properly_loads_extras(locker: Locker) -> None: description = "httplib2 caching for requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] files = [] [package.dependencies] @@ -219,7 +238,7 @@ def test_locker_properly_loads_extras(locker: Locker) -> None: redis = ["redis (>=2.10.5)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "~2.7 || ^3.4" content-hash = "c3d07fca33fba542ef2b2a4d75bf5b48d892d21a830e2ad9c952ba5123a52f77" """ @@ -249,6 +268,7 @@ def test_locker_properly_loads_nested_extras(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -263,6 +283,7 @@ def test_locker_properly_loads_nested_extras(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -277,11 +298,12 @@ def test_locker_properly_loads_nested_extras(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" """ @@ -327,6 +349,7 @@ def test_locker_properly_loads_extras_legacy(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -341,11 +364,12 @@ def test_locker_properly_loads_extras_legacy(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] python-versions = "*" -lock-version = "2.0" +lock-version = "2.1" content-hash = "123456789" """ @@ -374,6 +398,7 @@ def test_locker_properly_loads_subdir(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] develop = false files = [] @@ -385,7 +410,7 @@ def test_locker_properly_loads_subdir(locker: Locker) -> None: subdirectory = "subdir" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -416,6 +441,7 @@ def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] develop = false [[package]] @@ -424,6 +450,7 @@ def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] develop = false [package.source] @@ -438,6 +465,7 @@ def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] develop = false [package.source] @@ -450,6 +478,7 @@ def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] develop = false [package.source] @@ -462,6 +491,7 @@ def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] develop = false [package.source] @@ -513,12 +543,12 @@ def test_locker_properly_assigns_metadata_files(locker: Locker) -> None: def test_lock_packages_with_null_description( - locker: Locker, root: ProjectPackage + locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: package_a = get_package("A", "1.0.0") package_a.description = None # type: ignore[assignment] - locker.set_lock_data(root, [package_a]) + locker.set_lock_data(root, {package_a: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content = f.read() @@ -532,10 +562,11 @@ def test_lock_packages_with_null_description( description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -544,7 +575,7 @@ def test_lock_packages_with_null_description( def test_lock_file_should_not_have_mixed_types( - locker: Locker, root: ProjectPackage + locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo ) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency(Factory.create_dependency("B", "^1.0.0")) @@ -554,7 +585,7 @@ def test_lock_file_should_not_have_mixed_types( package_a.requires[-1].activate() package_a.extras = {canonicalize_name("foo"): [get_dependency("B", ">=1.0.0")]} - locker.set_lock_data(root, [package_a]) + locker.set_lock_data(root, {package_a: transitive_info}) expected = f"""\ # {GENERATED_COMMENT} @@ -565,6 +596,7 @@ def test_lock_file_should_not_have_mixed_types( description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -577,7 +609,7 @@ def test_lock_file_should_not_have_mixed_types( foo = ["B (>=1.0.0)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -600,6 +632,7 @@ def test_reading_lock_file_should_raise_an_error_on_invalid_data( description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.extras] @@ -609,7 +642,7 @@ def test_reading_lock_file_should_raise_an_error_on_invalid_data( foo = ["bar"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -634,6 +667,7 @@ def test_reading_lock_file_should_raise_an_error_on_missing_metadata( description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.source] @@ -654,7 +688,7 @@ def test_reading_lock_file_should_raise_an_error_on_missing_metadata( def test_locking_legacy_repository_package_should_include_source_section( - root: ProjectPackage, locker: Locker + root: ProjectPackage, locker: Locker, transitive_info: TransitivePackageInfo ) -> None: package_a = Package( "A", @@ -663,7 +697,7 @@ def test_locking_legacy_repository_package_should_include_source_section( source_url="https://foo.bar", source_reference="legacy", ) - packages = [package_a] + packages = {package_a: transitive_info} locker.set_lock_data(root, packages) @@ -679,6 +713,7 @@ def test_locking_legacy_repository_package_should_include_source_section( description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.source] @@ -687,7 +722,7 @@ def test_locking_legacy_repository_package_should_include_source_section( reference = "legacy" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -776,7 +811,7 @@ def test_root_extras_dependencies_are_ordered( canonicalize_name("C"): [package_third, package_second, package_first], canonicalize_name("B"): [package_first, package_second, package_third], } - locker.set_lock_data(root, []) + locker.set_lock_data(root, {}) expected = f"""\ # {GENERATED_COMMENT} @@ -787,7 +822,7 @@ def test_root_extras_dependencies_are_ordered( c = ["first", "second", "third"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -798,7 +833,9 @@ def test_root_extras_dependencies_are_ordered( assert content == expected -def test_extras_dependencies_are_ordered(locker: Locker, root: ProjectPackage) -> None: +def test_extras_dependencies_are_ordered( + locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo +) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency( Factory.create_dependency( @@ -807,7 +844,7 @@ def test_extras_dependencies_are_ordered(locker: Locker, root: ProjectPackage) - ) package_a.requires[-1].activate() - locker.set_lock_data(root, [package_a]) + locker.set_lock_data(root, {package_a: transitive_info}) expected = f"""\ # {GENERATED_COMMENT} @@ -818,13 +855,14 @@ def test_extras_dependencies_are_ordered(locker: Locker, root: ProjectPackage) - description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] B = {{version = "^1.0.0", extras = ["a", "b", "c"], optional = true}} [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -857,8 +895,128 @@ def test_locker_should_neither_emit_warnings_nor_raise_error_for_lower_compatibl assert len(caplog.records) == 0 +def test_locker_dumps_groups_and_markers( + locker: Locker, + root: ProjectPackage, + fixture_base: Path, + transitive_info: TransitivePackageInfo, +) -> None: + packages = { + get_package("A", "1.0"): TransitivePackageInfo( + 0, {"main"}, {"main": AnyMarker()} + ), + get_package("B", "1.0"): TransitivePackageInfo( + 0, {"main"}, {"main": parse_marker('sys_platform == "win32"')} + ), + get_package("C", "1.0"): TransitivePackageInfo( + 0, {"main", "dev"}, {"main": AnyMarker(), "dev": AnyMarker()} + ), + get_package("D", "1.0"): TransitivePackageInfo( + 0, + {"main", "dev"}, + { + "main": parse_marker('sys_platform == "win32"'), + "dev": parse_marker('sys_platform == "win32"'), + }, + ), + get_package("E", "1.0"): TransitivePackageInfo( + 0, + {"main", "dev"}, + { + "main": parse_marker('sys_platform == "win32"'), + "dev": parse_marker('sys_platform == "linux"'), + }, + ), + get_package("F", "1.0"): TransitivePackageInfo( + 0, + {"main", "dev"}, + { + "main": parse_marker('sys_platform == "win32"'), + "dev": AnyMarker(), + }, + ), + } + + locker.set_lock_data(root, packages) + + with locker.lock.open(encoding="utf-8") as f: + content = f.read() + + expected = f"""\ +# {GENERATED_COMMENT} + +[[package]] +name = "A" +version = "1.0" +description = "" +optional = false +python-versions = "*" +groups = ["main"] +files = [] + +[[package]] +name = "B" +version = "1.0" +description = "" +optional = false +python-versions = "*" +groups = ["main"] +markers = "sys_platform == \\"win32\\"" +files = [] + +[[package]] +name = "C" +version = "1.0" +description = "" +optional = false +python-versions = "*" +groups = ["main", "dev"] +files = [] + +[[package]] +name = "D" +version = "1.0" +description = "" +optional = false +python-versions = "*" +groups = ["main", "dev"] +markers = "sys_platform == \\"win32\\"" +files = [] + +[[package]] +name = "E" +version = "1.0" +description = "" +optional = false +python-versions = "*" +groups = ["main", "dev"] +files = [] +markers = {{main = "sys_platform == \\"win32\\"", dev = "sys_platform == \\"linux\\""}} + +[[package]] +name = "F" +version = "1.0" +description = "" +optional = false +python-versions = "*" +groups = ["main", "dev"] +files = [] +markers = {{main = "sys_platform == \\"win32\\""}} + +[metadata] +lock-version = "2.1" +python-versions = "*" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" +""" + + assert content == expected + + def test_locker_dumps_dependency_information_correctly( - locker: Locker, root: ProjectPackage, fixture_base: Path + locker: Locker, + root: ProjectPackage, + fixture_base: Path, + transitive_info: TransitivePackageInfo, ) -> None: package_a = get_package("A", "1.0.0") package_a.add_dependency( @@ -908,7 +1066,7 @@ def test_locker_dumps_dependency_information_correctly( ) ) - packages = [package_a] + packages = {package_a: transitive_info} locker.set_lock_data(root, packages) @@ -924,6 +1082,7 @@ def test_locker_dumps_dependency_information_correctly( description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.dependencies] @@ -937,7 +1096,7 @@ def test_locker_dumps_dependency_information_correctly( I = {{git = "https://github.com/python-poetry/poetry.git", rev = "spam"}} [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -945,7 +1104,9 @@ def test_locker_dumps_dependency_information_correctly( assert content == expected -def test_locker_dumps_subdir(locker: Locker, root: ProjectPackage) -> None: +def test_locker_dumps_subdir( + locker: Locker, root: ProjectPackage, transitive_info: TransitivePackageInfo +) -> None: package_git_with_subdirectory = Package( "git-package-subdir", "1.2.3", @@ -956,7 +1117,7 @@ def test_locker_dumps_subdir(locker: Locker, root: ProjectPackage) -> None: source_subdirectory="subdir", ) - locker.set_lock_data(root, [package_git_with_subdirectory]) + locker.set_lock_data(root, {package_git_with_subdirectory: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content = f.read() @@ -970,6 +1131,7 @@ def test_locker_dumps_subdir(locker: Locker, root: ProjectPackage) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] files = [] develop = false @@ -981,7 +1143,7 @@ def test_locker_dumps_subdir(locker: Locker, root: ProjectPackage) -> None: subdirectory = "subdir" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -990,7 +1152,10 @@ def test_locker_dumps_subdir(locker: Locker, root: ProjectPackage) -> None: def test_locker_dumps_dependency_extras_in_correct_order( - locker: Locker, root: ProjectPackage, fixture_base: Path + locker: Locker, + root: ProjectPackage, + fixture_base: Path, + transitive_info: TransitivePackageInfo, ) -> None: package_a = get_package("A", "1.0.0") Factory.create_dependency("B", "1.0.0", root_dir=fixture_base) @@ -1004,7 +1169,7 @@ def test_locker_dumps_dependency_extras_in_correct_order( canonicalize_name("B"): [package_first, package_second, package_third], } - locker.set_lock_data(root, [package_a]) + locker.set_lock_data(root, {package_a: transitive_info}) with locker.lock.open(encoding="utf-8") as f: content = f.read() @@ -1018,6 +1183,7 @@ def test_locker_dumps_dependency_extras_in_correct_order( description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.extras] @@ -1025,7 +1191,7 @@ def test_locker_dumps_dependency_extras_in_correct_order( c = ["first (==1.0.0)", "second (==1.0.0)", "third (==1.0.0)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -1045,6 +1211,7 @@ def test_locked_repository_uses_root_dir_of_package( description = "" optional = false python-versions = "^2.7.9" +groups = ["main"] develop = true file = [] @@ -1056,7 +1223,7 @@ def test_locked_repository_uses_root_dir_of_package( url = "lib/libA" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -1122,7 +1289,9 @@ def test_content_hash_with_legacy_is_compatible( assert (content_hash == old_content_hash) or fresh -def test_lock_file_resolves_file_url_symlinks(root: ProjectPackage) -> None: +def test_lock_file_resolves_file_url_symlinks( + root: ProjectPackage, transitive_info: TransitivePackageInfo +) -> None: """ Create directories and file structure as follows: @@ -1165,9 +1334,9 @@ def test_lock_file_resolves_file_url_symlinks(root: ProjectPackage) -> None: source_reference="develop", source_resolved_reference="123456", ) - packages = [ - package_local, - ] + packages = { + package_local: transitive_info, + } locker.set_lock_data(root, packages) @@ -1183,6 +1352,7 @@ def test_lock_file_resolves_file_url_symlinks(root: ProjectPackage) -> None: description = "" optional = false python-versions = "*" +groups = ["main"] files = [] [package.source] @@ -1199,7 +1369,7 @@ def test_lock_file_resolves_file_url_symlinks(root: ProjectPackage) -> None: resolved_reference = "123456" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -1217,7 +1387,7 @@ def test_lockfile_is_not_rewritten_if_only_poetry_version_changed( package = [] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "*" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" """ @@ -1225,7 +1395,7 @@ def test_lockfile_is_not_rewritten_if_only_poetry_version_changed( with open(locker.lock, "w", encoding="utf-8") as f: f.write(old_content) - assert not locker.set_lock_data(root, []) + assert not locker.set_lock_data(root, {}) with locker.lock.open(encoding="utf-8") as f: content = f.read() diff --git a/tests/puzzle/conftest.py b/tests/puzzle/conftest.py index 9023893a149..af47e4d4c3e 100644 --- a/tests/puzzle/conftest.py +++ b/tests/puzzle/conftest.py @@ -4,6 +4,12 @@ import pytest +from cleo.io.null_io import NullIO +from poetry.core.packages.project_package import ProjectPackage + +from poetry.puzzle import Solver +from poetry.repositories import Repository +from poetry.repositories import RepositoryPool from tests.helpers import MOCK_DEFAULT_GIT_REVISION from tests.helpers import mock_clone @@ -18,3 +24,28 @@ def setup(mocker: MockerFixture) -> None: mocker.patch("poetry.vcs.git.Git.clone", new=mock_clone) p = mocker.patch("poetry.vcs.git.Git.get_revision") p.return_value = MOCK_DEFAULT_GIT_REVISION + + +@pytest.fixture +def io() -> NullIO: + return NullIO() + + +@pytest.fixture +def package() -> ProjectPackage: + return ProjectPackage("root", "1.0") + + +@pytest.fixture +def repo() -> Repository: + return Repository("repo") + + +@pytest.fixture +def pool(repo: Repository) -> RepositoryPool: + return RepositoryPool([repo]) + + +@pytest.fixture +def solver(package: ProjectPackage, pool: RepositoryPool, io: NullIO) -> Solver: + return Solver(package, pool, [], [], io) diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 36f41d88b05..dea4d04af1c 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -10,11 +10,9 @@ import pytest from cleo.io.buffered_io import BufferedIO -from cleo.io.null_io import NullIO from packaging.utils import canonicalize_name from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package -from poetry.core.packages.project_package import ProjectPackage from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.version.markers import parse_marker @@ -36,6 +34,8 @@ if TYPE_CHECKING: import httpretty + from cleo.io.null_io import NullIO + from poetry.core.packages.project_package import ProjectPackage from pytest_mock import MockerFixture from poetry.installation.operations.operation import Operation @@ -56,31 +56,6 @@ def set_package_python_versions(provider: Provider, python_versions: str) -> Non provider._python_constraint = provider._package.python_constraint -@pytest.fixture() -def io() -> NullIO: - return NullIO() - - -@pytest.fixture() -def package() -> ProjectPackage: - return ProjectPackage("root", "1.0") - - -@pytest.fixture() -def repo() -> Repository: - return Repository("repo") - - -@pytest.fixture() -def pool(repo: Repository) -> RepositoryPool: - return RepositoryPool([repo]) - - -@pytest.fixture() -def solver(package: ProjectPackage, pool: RepositoryPool, io: NullIO) -> Solver: - return Solver(package, pool, [], [], io) - - def check_solver_result( transaction: Transaction, expected: list[dict[str, Any]], @@ -786,7 +761,7 @@ def test_solver_merge_extras_into_base_package_multiple_repos_fixes_5727( package_b = Package("B", "1.0", source_type="legacy") package_b.add_dependency(package_a.with_features(["foo"]).to_dependency()) - package_a = Package("A", "1.0", source_type="legacy") + package_a = Package("A", "1.0", source_type="legacy", source_reference="legacy") package_a.extras = {canonicalize_name("foo"): []} repo = Repository("legacy") @@ -3370,7 +3345,9 @@ def test_direct_dependency_with_extras_from_explicit_and_transitive_dependency( repo.add_package(package_extra) # extra only in default repo for version in lib_versions: - package_lib = get_package("lib", version) + package_lib = Package( + "lib", version, source_type="legacy", source_reference="explicit" + ) dep_extra = get_dependency("extra", ">=1.0") package_lib.add_dependency( @@ -3381,7 +3358,9 @@ def test_direct_dependency_with_extras_from_explicit_and_transitive_dependency( explicit_repo.add_package(package_lib) # lib only in explicit repo for version in other_versions: - package_other = get_package("other", version) + package_other = Package( + "other", version, source_type="legacy", source_reference="explicit" + ) package_other.add_dependency(Factory.create_dependency("lib", ">=1.0")) explicit_repo.add_package(package_other) # other only in explicit repo @@ -3389,12 +3368,18 @@ def test_direct_dependency_with_extras_from_explicit_and_transitive_dependency( transaction = solver.solve() + expected_lib = Package( + "lib", "2.0", source_type="legacy", source_reference="explicit" + ) + expected_other = Package( + "other", "2.0", source_type="legacy", source_reference="explicit" + ) check_solver_result( transaction, [ {"job": "install", "package": get_package("extra", "1.0")}, - {"job": "install", "package": get_package("lib", "2.0")}, - {"job": "install", "package": get_package("other", "2.0")}, + {"job": "install", "package": expected_lib}, + {"job": "install", "package": expected_other}, ], ) @@ -3441,7 +3426,9 @@ def test_direct_dependency_with_extras_from_explicit_and_transitive_dependency2( repo.add_package(package_other_extra) # extra only in default repo for version in lib_versions: - package_lib = get_package("lib", version) + package_lib = Package( + "lib", version, source_type="legacy", source_reference="explicit" + ) dep_extra = get_dependency("extra", ">=1.0") package_lib.add_dependency( @@ -3462,7 +3449,9 @@ def test_direct_dependency_with_extras_from_explicit_and_transitive_dependency2( explicit_repo.add_package(package_lib) # lib only in explicit repo for version in other_versions: - package_other = get_package("other", version) + package_other = Package( + "other", version, source_type="legacy", source_reference="explicit" + ) package_other.add_dependency( Factory.create_dependency( "lib", {"version": ">=1.0", "extras": ["other-extra"]} @@ -3474,13 +3463,19 @@ def test_direct_dependency_with_extras_from_explicit_and_transitive_dependency2( transaction = solver.solve() + expected_lib = Package( + "lib", "2.0", source_type="legacy", source_reference="explicit" + ) + expected_other = Package( + "other", "2.0", source_type="legacy", source_reference="explicit" + ) check_solver_result( transaction, [ {"job": "install", "package": get_package("other-extra", "1.0")}, {"job": "install", "package": get_package("extra", "1.0")}, - {"job": "install", "package": get_package("lib", "2.0")}, - {"job": "install", "package": get_package("other", "2.0")}, + {"job": "install", "package": expected_lib}, + {"job": "install", "package": expected_other}, ], ) @@ -3966,7 +3961,12 @@ def test_solver_cannot_choose_url_dependency_for_explicit_source( ) package_pendulum = get_package("pendulum", "1.4.4") - package_demo = get_package("demo", "0.1.0") + package_demo = Package( + "demo", + "0.1.0", + source_type="legacy" if explicit_source else None, + source_reference="repo" if explicit_source else None, + ) package_demo_url = Package( "demo", "0.1.0", diff --git a/tests/puzzle/test_solver_internals.py b/tests/puzzle/test_solver_internals.py new file mode 100644 index 00000000000..b7f13c5a041 --- /dev/null +++ b/tests/puzzle/test_solver_internals.py @@ -0,0 +1,495 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Iterable +from typing import Sequence + +from packaging.utils import canonicalize_name +from poetry.core.packages.dependency import Dependency +from poetry.core.packages.package import Package +from poetry.core.version.markers import AnyMarker +from poetry.core.version.markers import parse_marker + +from poetry.factory import Factory +from poetry.packages.transitive_package_info import TransitivePackageInfo +from poetry.puzzle.solver import PackageNode +from poetry.puzzle.solver import Solver +from poetry.puzzle.solver import depth_first_search +from poetry.puzzle.solver import merge_packages_from_override + + +if TYPE_CHECKING: + from poetry.core.packages.project_package import ProjectPackage + + +def dep( + name: str, + marker: str = "", + extras: Iterable[str] = (), + in_extras: Sequence[str] = (), + groups: Iterable[str] = (), +) -> Dependency: + d = Dependency(name, "1", groups=groups, extras=extras) + d._in_extras = [canonicalize_name(e) for e in in_extras] + if marker: + d.marker = marker # type: ignore[assignment] + return d + + +def tm(info: TransitivePackageInfo) -> dict[str, str]: + return {key: str(value) for key, value in info.markers.items()} + + +def test_dfs_depth(package: ProjectPackage) -> None: + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + packages = [package, a, b, c] + package.add_dependency(dep("a")) + package.add_dependency(dep("b")) + a.add_dependency(dep("b")) + b.add_dependency(dep("c")) + + result, __ = depth_first_search(PackageNode(package, packages)) + depths = { + nodes[0].package.complete_name: [node.depth for node in nodes] + for nodes in result + } + + assert depths == {"root": [-1], "a": [0], "b": [1], "c": [2]} + + +def test_dfs_depth_with_cycle(package: ProjectPackage) -> None: + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + packages = [package, a, b, c] + package.add_dependency(dep("a")) + package.add_dependency(dep("b")) + a.add_dependency(dep("b")) + b.add_dependency(dep("a")) + a.add_dependency(dep("c")) + + result, __ = depth_first_search(PackageNode(package, packages)) + depths = { + nodes[0].package.complete_name: [node.depth for node in nodes] + for nodes in result + } + + assert depths == {"root": [-1], "a": [0], "b": [1], "c": [1]} + + +def test_dfs_depth_with_extra(package: ProjectPackage) -> None: + a_foo = Package("a", "1", features=["foo"]) + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + packages = [package, a_foo, a, b, c] + package.add_dependency(dep("a", extras=["foo"])) + a_foo.add_dependency(dep("a")) + a_foo.add_dependency(dep("b")) + a_foo.add_dependency(dep("c", 'extra == "foo"')) + a.add_dependency(dep("b")) + + result, __ = depth_first_search(PackageNode(package, packages)) + depths = { + nodes[0].package.complete_name: [node.depth for node in nodes] + for nodes in result + } + + assert depths == {"root": [-1], "a[foo]": [0], "a": [0], "b": [1], "c": [1]} + + +def test_propagate_markers(package: ProjectPackage, solver: Solver) -> None: + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + d = Package("d", "1") + e = Package("e", "1") + package.add_dependency(dep("a", 'sys_platform == "win32"')) + package.add_dependency(dep("b", 'sys_platform == "linux"')) + a.add_dependency(dep("c", 'python_version == "3.8"')) + b.add_dependency(dep("d", 'python_version == "3.9"')) + a.add_dependency(dep("e", 'python_version == "3.10"')) + b.add_dependency(dep("e", 'python_version == "3.11"')) + + packages = [package, a, b, c, d, e] + result = solver._aggregate_solved_packages(packages) + + assert len(result) == 6 + assert tm(result[package]) == {} + assert tm(result[a]) == {"main": 'sys_platform == "win32"'} + assert tm(result[b]) == {"main": 'sys_platform == "linux"'} + assert tm(result[c]) == { + "main": 'sys_platform == "win32" and python_version == "3.8"' + } + assert tm(result[d]) == { + "main": 'sys_platform == "linux" and python_version == "3.9"' + } + assert tm(result[e]) == { + "main": 'sys_platform == "win32" and python_version == "3.10"' + ' or sys_platform == "linux" and python_version == "3.11"' + } + + +def test_propagate_markers_same_name(package: ProjectPackage, solver: Solver) -> None: + urls = { + "linux": "https://files.pythonhosted.org/distributions/demo-0.1.0.tar.gz", + "win32": ( + "https://files.pythonhosted.org/distributions/demo-0.1.0-py2.py3-none-any.whl" + ), + } + sdist = Package("demo", "0.1.0", source_type="url", source_url=urls["linux"]) + wheel = Package("demo", "0.1.0", source_type="url", source_url=urls["win32"]) + for platform, url in urls.items(): + package.add_dependency( + Factory.create_dependency( + "demo", + {"url": url, "markers": f"sys_platform == '{platform}'"}, + ) + ) + + packages = [package, sdist, wheel] + result = solver._aggregate_solved_packages(packages) + + assert len(result) == 3 + assert tm(result[package]) == {} + assert tm(result[sdist]) == {"main": 'sys_platform == "linux"'} + assert tm(result[wheel]) == {"main": 'sys_platform == "win32"'} + + +def test_propagate_markers_with_extra(package: ProjectPackage, solver: Solver) -> None: + a_foo = Package("a", "1", features=["foo"]) + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + d = Package("d", "1") + package.add_dependency(dep("a", 'sys_platform == "win32"', extras=["foo"])) + package.add_dependency(dep("b", 'sys_platform == "linux"')) + a_foo.add_dependency(dep("a")) + a_foo.add_dependency(dep("c", 'python_version == "3.8"')) + a_foo.add_dependency(dep("d", 'extra == "foo"')) + a.add_dependency(dep("c", 'python_version == "3.8"')) + b.add_dependency(dep("a", 'python_version == "3.9"')) + + packages = [package, a_foo, a, b, c, d] + result = solver._aggregate_solved_packages(packages) + + assert len(result) == len(packages) - 1 + assert tm(result[package]) == {} + assert tm(result[a]) == { + "main": ( + 'sys_platform == "linux" and python_version == "3.9" or sys_platform == "win32"' + ) + } + assert tm(result[b]) == {"main": 'sys_platform == "linux"'} + assert tm(result[c]) == { + "main": 'sys_platform == "win32" and python_version == "3.8"' + } + assert tm(result[d]) == {"main": 'sys_platform == "win32"'} + + +def test_propagate_markers_with_root_extra( + package: ProjectPackage, solver: Solver +) -> None: + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + d = Package("d", "1") + # "extra" is not present in the marker of an extra dependency of the root package, + # there is only "in_extras"... + package.add_dependency(dep("a", in_extras=["foo"])) + package.add_dependency( + dep("b", 'sys_platform == "linux"', in_extras=["foo", "bar"]) + ) + a.add_dependency(dep("c", 'python_version == "3.8"')) + b.add_dependency(dep("d", 'python_version == "3.9"')) + + packages = [package, a, b, c, d] + result = solver._aggregate_solved_packages(packages) + + assert len(result) == len(packages) + assert tm(result[package]) == {} + assert tm(result[a]) == {"main": 'extra == "foo"'} + assert tm(result[b]) == { + "main": ( + 'extra == "foo" and sys_platform == "linux"' + ' or extra == "bar" and sys_platform == "linux"' + ) + } + assert tm(result[c]) == {"main": 'extra == "foo" and python_version == "3.8"'} + assert tm(result[d]) == { + "main": ( + 'extra == "foo" and sys_platform == "linux" and python_version == "3.9"' + ' or extra == "bar" and sys_platform == "linux" and python_version == "3.9"' + ) + } + + +def test_propagate_groups_with_extra(package: ProjectPackage, solver: Solver) -> None: + a_foo = Package("a", "1", features=["foo"]) + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + package.add_dependency(dep("a", groups=["main"])) + package.add_dependency(dep("a", groups=["dev"], extras=["foo"])) + a_foo.add_dependency(dep("a")) + a_foo.add_dependency(dep("b")) + a_foo.add_dependency(dep("c", 'extra == "foo"')) + a.add_dependency(dep("b")) + + packages = [package, a_foo, a, b, c] + result = solver._aggregate_solved_packages(packages) + + assert len(result) == len(packages) - 1 + assert result[package].groups == set() + assert result[a].groups == {"main", "dev"} + assert result[b].groups == {"main", "dev"} + assert result[c].groups == {"dev"} + + +def test_propagate_markers_for_groups1(package: ProjectPackage, solver: Solver) -> None: + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + package.add_dependency(dep("a", 'sys_platform == "win32"', groups=["main"])) + package.add_dependency(dep("b", 'sys_platform == "linux"', groups=["dev"])) + a.add_dependency(dep("c", 'python_version == "3.8"')) + b.add_dependency(dep("c", 'python_version == "3.9"')) + + packages = [package, a, b, c] + result = solver._aggregate_solved_packages(packages) + + assert len(result) == len(packages) + assert result[package].groups == set() + assert result[a].groups == {"main"} + assert result[b].groups == {"dev"} + assert result[c].groups == {"main", "dev"} + assert tm(result[package]) == {} + assert tm(result[a]) == {"main": 'sys_platform == "win32"'} + assert tm(result[b]) == {"dev": 'sys_platform == "linux"'} + assert tm(result[c]) == { + "main": 'sys_platform == "win32" and python_version == "3.8"', + "dev": 'sys_platform == "linux" and python_version == "3.9"', + } + + +def test_propagate_markers_for_groups2(package: ProjectPackage, solver: Solver) -> None: + a = Package("a", "1") + b = Package("b", "1") + c = Package("c", "1") + d = Package("d", "1") + package.add_dependency(dep("a", 'sys_platform == "win32"', groups=["main"])) + package.add_dependency(dep("b", 'sys_platform == "linux"', groups=["dev"])) + package.add_dependency(dep("c", 'sys_platform == "darwin"', groups=["main", "dev"])) + a.add_dependency(dep("d", 'python_version == "3.8"')) + b.add_dependency(dep("d", 'python_version == "3.9"')) + c.add_dependency(dep("d", 'python_version == "3.10"')) + + packages = [package, a, b, c, d] + result = solver._aggregate_solved_packages(packages) + + assert len(result) == len(packages) + assert result[package].groups == set() + assert result[a].groups == {"main"} + assert result[b].groups == {"dev"} + assert result[c].groups == {"main", "dev"} + assert result[d].groups == {"main", "dev"} + assert tm(result[package]) == {} + assert tm(result[a]) == {"main": 'sys_platform == "win32"'} + assert tm(result[b]) == {"dev": 'sys_platform == "linux"'} + assert tm(result[c]) == { + "main": 'sys_platform == "darwin"', + "dev": 'sys_platform == "darwin"', + } + assert tm(result[d]) == { + "main": ( + 'sys_platform == "win32" and python_version == "3.8"' + ' or sys_platform == "darwin" and python_version == "3.10"' + ), + "dev": ( + 'sys_platform == "darwin" and python_version == "3.10"' + ' or sys_platform == "linux" and python_version == "3.9"' + ), + } + + +def test_propagate_markers_with_cycle(package: ProjectPackage, solver: Solver) -> None: + a = Package("a", "1") + b = Package("b", "1") + package.add_dependency(dep("a", 'sys_platform == "win32"')) + package.add_dependency(dep("b", 'sys_platform == "linux"')) + a.add_dependency(dep("b", 'python_version == "3.8"')) + b.add_dependency(dep("a", 'python_version == "3.9"')) + + packages = [package, a, b] + result = solver._aggregate_solved_packages(packages) + + assert len(result) == 3 + assert tm(result[package]) == {} + assert tm(result[a]) == { + "main": ( + 'sys_platform == "linux" and python_version == "3.9"' + ' or sys_platform == "win32"' + ) + } + assert tm(result[b]) == { + "main": ( + 'sys_platform == "win32" and python_version == "3.8"' + ' or sys_platform == "linux"' + ) + } + + +def test_merge_packages_from_override_restricted(package: ProjectPackage) -> None: + """Markers of depedencies should be intersected with override markers.""" + a = Package("a", "1") + + packages: dict[Package, TransitivePackageInfo] = {} + merge_packages_from_override( + packages, + { + a: TransitivePackageInfo( + 0, {"main"}, {"main": parse_marker("sys_platform == 'win32'")} + ) + }, + {package: {"a": dep("b", 'python_version < "3.9"')}}, + ) + merge_packages_from_override( + packages, + { + a: TransitivePackageInfo( + 0, {"main"}, {"main": parse_marker("sys_platform == 'linux'")} + ) + }, + {package: {"a": dep("b", 'python_version >= "3.9"')}}, + ) + assert len(packages) == 1 + assert packages[a].groups == {"main"} + assert tm(packages[a]) == { + "main": ( + 'python_version < "3.9" and sys_platform == "win32"' + ' or sys_platform == "linux" and python_version >= "3.9"' + ) + } + + +def test_merge_packages_from_override_extras(package: ProjectPackage) -> None: + """Extras from overrides should not be visible in the resulting marker.""" + a = Package("a", "1") + + packages: dict[Package, TransitivePackageInfo] = {} + merge_packages_from_override( + packages, + { + a: TransitivePackageInfo( + 0, {"main"}, {"main": parse_marker("sys_platform == 'win32'")} + ) + }, + {package: {"a": dep("b", 'python_version < "3.9" and extra == "foo"')}}, + ) + merge_packages_from_override( + packages, + { + a: TransitivePackageInfo( + 0, {"main"}, {"main": parse_marker("sys_platform == 'linux'")} + ) + }, + {package: {"a": dep("b", 'python_version >= "3.9" and extra == "foo"')}}, + ) + assert len(packages) == 1 + assert packages[a].groups == {"main"} + assert tm(packages[a]) == { + "main": ( + 'python_version < "3.9" and sys_platform == "win32"' + ' or sys_platform == "linux" and python_version >= "3.9"' + ) + } + + +def test_merge_packages_from_override_multiple_deps(package: ProjectPackage) -> None: + """All override markers should be intersected.""" + a = Package("a", "1") + + packages: dict[Package, TransitivePackageInfo] = {} + merge_packages_from_override( + packages, + {a: TransitivePackageInfo(0, {"main"}, {"main": AnyMarker()})}, + { + package: { + "a": dep("b", 'python_version < "3.9"'), + "c": dep("d", 'sys_platform == "linux"'), + }, + a: {"e": dep("f", 'python_version >= "3.8"')}, + }, + ) + + assert len(packages) == 1 + assert packages[a].groups == {"main"} + assert tm(packages[a]) == { + "main": ( + 'python_version < "3.9" and sys_platform == "linux"' + ' and python_version >= "3.8"' + ) + } + + +def test_merge_packages_from_override_groups(package: ProjectPackage) -> None: + a = Package("a", "1") + b = Package("b", "1") + + packages: dict[Package, TransitivePackageInfo] = {} + merge_packages_from_override( + packages, + { + a: TransitivePackageInfo( + 0, {"main"}, {"main": parse_marker("sys_platform == 'win32'")} + ), + b: TransitivePackageInfo( + 0, + {"main", "dev"}, + { + "main": parse_marker("sys_platform == 'win32'"), + "dev": parse_marker("sys_platform == 'linux'"), + }, + ), + }, + {package: {"a": dep("b", 'python_version < "3.9"')}}, + ) + merge_packages_from_override( + packages, + { + a: TransitivePackageInfo( + 0, {"dev"}, {"dev": parse_marker("sys_platform == 'linux'")} + ), + b: TransitivePackageInfo( + 0, + {"main", "dev"}, + { + "main": parse_marker("platform_machine == 'amd64'"), + "dev": parse_marker("platform_machine == 'aarch64'"), + }, + ), + }, + {package: {"a": dep("b", 'python_version >= "3.9"')}}, + ) + assert len(packages) == 2 + assert packages[a].groups == {"main", "dev"} + assert tm(packages[a]) == { + "main": 'python_version < "3.9" and sys_platform == "win32"', + "dev": 'python_version >= "3.9" and sys_platform == "linux"', + } + assert packages[b].groups == {"main", "dev"} + assert tm(packages[b]) == { + "main": ( + 'python_version < "3.9" and sys_platform == "win32"' + ' or python_version >= "3.9" and platform_machine == "amd64"' + ), + "dev": ( + 'python_version < "3.9" and sys_platform == "linux"' + ' or python_version >= "3.9" and platform_machine == "aarch64"' + ), + } + + +# TODO: root extras diff --git a/tests/puzzle/test_transaction.py b/tests/puzzle/test_transaction.py index 05b19649c60..78640a54813 100644 --- a/tests/puzzle/test_transaction.py +++ b/tests/puzzle/test_transaction.py @@ -6,6 +6,7 @@ from poetry.core.packages.package import Package from poetry.installation.operations.update import Update +from poetry.packages.transitive_package_info import TransitivePackageInfo from poetry.puzzle.transaction import Transaction @@ -13,6 +14,10 @@ from poetry.installation.operations.operation import Operation +def get_transitive_info(depth: int) -> TransitivePackageInfo: + return TransitivePackageInfo(depth, set(), {}) + + def check_operations(ops: list[Operation], expected: list[dict[str, Any]]) -> None: for e in expected: if "skipped" not in e: @@ -43,11 +48,11 @@ def check_operations(ops: list[Operation], expected: list[dict[str, Any]]) -> No def test_it_should_calculate_operations_in_correct_order() -> None: transaction = Transaction( [Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")], - [ - (Package("a", "1.0.0"), 1), - (Package("b", "2.1.0"), 2), - (Package("d", "4.0.0"), 0), - ], + { + Package("a", "1.0.0"): get_transitive_info(1), + Package("b", "2.1.0"): get_transitive_info(2), + Package("d", "4.0.0"): get_transitive_info(0), + }, ) check_operations( @@ -63,11 +68,11 @@ def test_it_should_calculate_operations_in_correct_order() -> None: def test_it_should_calculate_operations_for_installed_packages() -> None: transaction = Transaction( [Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")], - [ - (Package("a", "1.0.0"), 1), - (Package("b", "2.1.0"), 2), - (Package("d", "4.0.0"), 0), - ], + { + Package("a", "1.0.0"): get_transitive_info(1), + Package("b", "2.1.0"): get_transitive_info(2), + Package("d", "4.0.0"): get_transitive_info(0), + }, installed_packages=[ Package("a", "1.0.0"), Package("b", "2.0.0"), @@ -94,11 +99,11 @@ def test_it_should_calculate_operations_for_installed_packages() -> None: def test_it_should_remove_installed_packages_if_required() -> None: transaction = Transaction( [Package("a", "1.0.0"), Package("b", "2.0.0"), Package("c", "3.0.0")], - [ - (Package("a", "1.0.0"), 1), - (Package("b", "2.1.0"), 2), - (Package("d", "4.0.0"), 0), - ], + { + Package("a", "1.0.0"): get_transitive_info(1), + Package("b", "2.1.0"): get_transitive_info(2), + Package("d", "4.0.0"): get_transitive_info(0), + }, installed_packages=[ Package("a", "1.0.0"), Package("b", "2.0.0"), @@ -126,11 +131,11 @@ def test_it_should_remove_installed_packages_if_required() -> None: def test_it_should_not_remove_installed_packages_that_are_in_result() -> None: transaction = Transaction( [], - [ - (Package("a", "1.0.0"), 1), - (Package("b", "2.0.0"), 2), - (Package("c", "3.0.0"), 0), - ], + { + Package("a", "1.0.0"): get_transitive_info(1), + Package("b", "2.0.0"): get_transitive_info(2), + Package("c", "3.0.0"): get_transitive_info(0), + }, installed_packages=[ Package("a", "1.0.0"), Package("b", "2.0.0"), @@ -151,19 +156,16 @@ def test_it_should_not_remove_installed_packages_that_are_in_result() -> None: def test_it_should_update_installed_packages_if_sources_are_different() -> None: transaction = Transaction( [Package("a", "1.0.0")], - [ - ( - Package( - "a", - "1.0.0", - source_url="https://github.com/demo/demo.git", - source_type="git", - source_reference="main", - source_resolved_reference="123456", - ), - 1, - ) - ], + { + Package( + "a", + "1.0.0", + source_url="https://github.com/demo/demo.git", + source_type="git", + source_reference="main", + source_resolved_reference="123456", + ): get_transitive_info(1) + }, installed_packages=[Package("a", "1.0.0")], )