diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index 474b1703b36..c15ce47865c 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -7,11 +7,13 @@ import time import urllib.parse +from collections import defaultdict from contextlib import contextmanager from pathlib import Path from tempfile import mkdtemp from typing import TYPE_CHECKING from typing import Any +from typing import Iterable from typing import Iterator from cleo.ui.progress_indicator import ProgressIndicator @@ -42,6 +44,8 @@ from poetry.core.packages.package import Package from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.vcs_dependency import VCSDependency + from poetry.core.semver.version_constraint import VersionConstraint + from poetry.core.version.markers import BaseMarker from poetry.repositories import Pool from poetry.utils.env import Env @@ -525,15 +529,12 @@ def complete_package(self, package: DependencyPackage) -> DependencyPackage: self.debug(f"Duplicate dependencies for {dep_name}") - # Regrouping by constraint - by_constraint: dict[str, list[Dependency]] = {} - for dep in deps: - if dep.constraint not in by_constraint: - by_constraint[dep.constraint] = [] + deps = self._merge_dependencies_by_marker(deps) + # Merging dependencies by constraint + by_constraint: dict[VersionConstraint, list[Dependency]] = defaultdict(list) + for dep in deps: by_constraint[dep.constraint].append(dep) - - # We merge by constraint for constraint, _deps in by_constraint.items(): new_markers = [] for dep in _deps: @@ -810,3 +811,29 @@ def progress(self) -> Iterator[None]: yield self._in_progress = False + + def _merge_dependencies_by_marker( + self, dependencies: Iterable[Dependency] + ) -> list[Dependency]: + by_marker: dict[BaseMarker, list[Dependency]] = defaultdict(list) + for dep in dependencies: + by_marker[dep.marker].append(dep) + deps = [] + for _deps in by_marker.values(): + if len(_deps) == 1: + deps.extend(_deps) + else: + new_constraint = _deps[0].constraint + for dep in _deps[1:]: + new_constraint = new_constraint.intersect(dep.constraint) + if new_constraint.is_empty(): + # leave dependencies as-is so the resolver will pickup + # the conflict and display a proper error. + deps.extend(_deps) + else: + self.debug( + f"Merging constraints for {_deps[0].name} for" + f" marker {_deps[0].marker}" + ) + deps.append(_deps[0].with_constraint(new_constraint)) + return deps diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 5f76e5d3f9d..87b4e2154bf 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -1300,6 +1300,43 @@ def test_solver_duplicate_dependencies_different_constraints_same_requirements( assert str(e.value) == expected +def test_solver_duplicate_dependencies_different_constraints_merge_by_marker( + solver: Solver, repo: Repository, package: Package +): + package.add_dependency(Factory.create_dependency("A", "*")) + + package_a = get_package("A", "1.0") + package_a.add_dependency( + Factory.create_dependency("B", {"version": "^1.0", "python": "<3.4"}) + ) + package_a.add_dependency( + Factory.create_dependency("B", {"version": "^2.0", "python": ">=3.4"}) + ) + package_a.add_dependency( + Factory.create_dependency("B", {"version": "!=1.1", "python": "<3.4"}) + ) + + package_b10 = get_package("B", "1.0") + package_b11 = get_package("B", "1.1") + package_b20 = get_package("B", "2.0") + + repo.add_package(package_a) + repo.add_package(package_b10) + repo.add_package(package_b11) + repo.add_package(package_b20) + + transaction = solver.solve() + + check_solver_result( + transaction, + [ + {"job": "install", "package": package_b10}, + {"job": "install", "package": package_b20}, + {"job": "install", "package": package_a}, + ], + ) + + def test_solver_duplicate_dependencies_different_constraints_merge_no_markers( solver: Solver, repo: Repository, package: Package ):