diff --git a/src/poetry/puzzle/solver.py b/src/poetry/puzzle/solver.py index 16432b3e297..dcfb7e626e5 100644 --- a/src/poetry/puzzle/solver.py +++ b/src/poetry/puzzle/solver.py @@ -179,16 +179,14 @@ def _solve(self) -> tuple[list[Package], list[int]]: if _package.name == dep.name: continue - try: - index = _package.requires.index(dep) - except ValueError: - _package.add_dependency(dep) - else: - _dep = _package.requires[index] - if _dep.marker != dep.marker: - # marker of feature package is more accurate - # because it includes relevant extras - _dep.marker = dep.marker + # Avoid duplication. + if any( + _dep == dep and _dep.marker == dep.marker + for _dep in _package.requires + ): + continue + + _package.add_dependency(dep) else: final_packages.append(package) depths.append(results[package]) diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 8cbbf15e36b..5c49557962d 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -663,6 +663,63 @@ def test_solver_returns_extras_when_multiple_extras_use_same_dependency( assert ops[0].package.marker.is_any() +def test_solver_locks_all_extras_when_multiple_extras_require_same_dependency( + solver: Solver, + repo: Repository, + package: ProjectPackage, +) -> None: + """ + - root depends on A[extra-b1] and C + - C depends on A[extra-b2] + - B is required by both extras + -> the locked dependency A on B must have both extra markers + """ + package_a = get_package("A", "1.0") + package_b = get_package("B", "1.0") + package_c = get_package("C", "1.0") + + dep_b1 = get_dependency("B", "*", optional=True) + dep_b1.marker = parse_marker("extra == 'extra-b1'") + + dep_b2 = get_dependency("B", "*", optional=True) + dep_b2.marker = parse_marker("extra == 'extra-b2'") + + package_a.extras = { + canonicalize_name("extra-b1"): [dep_b1], + canonicalize_name("extra-b2"): [dep_b2], + } + package_a.add_dependency(dep_b1) + package_a.add_dependency(dep_b2) + + package.add_dependency( + get_dependency("A", {"version": "*", "extras": ["extra-b1"]}) + ) + package.add_dependency(get_dependency("C", "*")) + package_c.add_dependency( + get_dependency("A", {"version": "*", "extras": ["extra-b2"]}) + ) + + repo.add_package(package_a) + repo.add_package(package_b) + repo.add_package(package_c) + + transaction = solver.solve() + + expected = [ + {"job": "install", "package": package_b}, + {"job": "install", "package": package_a}, + {"job": "install", "package": package_c}, + ] + + ops = check_solver_result(transaction, expected) + locked_a_requires = ops[1].package.requires + assert len(locked_a_requires) == 2 + assert {str(r.marker) for r in locked_a_requires} == { + 'extra == "extra-b1"', + 'extra == "extra-b2"', + } + + @pytest.mark.parametrize("enabled_extra", ["one", "two", None]) def test_solver_returns_extras_only_requested_nested( solver: Solver,