diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py
index c7fc3ff527b..09fbe8677b4 100644
--- a/src/poetry/puzzle/provider.py
+++ b/src/poetry/puzzle/provider.py
@@ -606,10 +606,11 @@ def complete_package(
# For dependency resolution, markers of duplicate dependencies must be
# mutually exclusive.
- deps = self._resolve_overlapping_markers(package, deps)
+ active_extras = None if package.is_root() else dependency.extras
+ deps = self._resolve_overlapping_markers(package, deps, active_extras)
if len(deps) == 1:
- self.debug(f"Merging requirements for {deps[0]!s}")
+ self.debug(f"Merging requirements for {dep_name}")
dependencies.append(deps[0])
continue
@@ -838,11 +839,14 @@ def _merge_dependencies_by_constraint(
return merged_dependencies
- def _is_relevant_marker(self, marker: BaseMarker) -> bool:
+ def _is_relevant_marker(
+ self, marker: BaseMarker, active_extras: Collection[NormalizedName] | None
+ ) -> bool:
"""
A marker is relevant if
- it is not empty
- allowed by the project's python constraint
+ - allowed by active extras of the dependency (not relevant for root package)
- allowed by the environment (only during installation)
"""
return (
@@ -850,11 +854,15 @@ def _is_relevant_marker(self, marker: BaseMarker) -> bool:
and self._python_constraint.allows_any(
get_python_constraint_from_marker(marker)
)
+ and (active_extras is None or marker.validate({"extra": active_extras}))
and (not self._env or marker.validate(self._env.marker_env))
)
def _resolve_overlapping_markers(
- self, package: Package, dependencies: list[Dependency]
+ self,
+ package: Package,
+ dependencies: list[Dependency],
+ active_extras: Collection[NormalizedName] | None,
) -> list[Dependency]:
"""
Convert duplicate dependencies with potentially overlapping markers
@@ -887,7 +895,7 @@ def _resolve_overlapping_markers(
used_marker_intersection: BaseMarker = AnyMarker()
for m in markers:
used_marker_intersection = used_marker_intersection.intersect(m)
- if not self._is_relevant_marker(used_marker_intersection):
+ if not self._is_relevant_marker(used_marker_intersection, active_extras):
continue
# intersection of constraints
diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py
index 4cf5b3943d0..0b85d824326 100644
--- a/tests/puzzle/test_solver.py
+++ b/tests/puzzle/test_solver.py
@@ -4322,3 +4322,91 @@ def test_update_with_use_latest_vs_lock(
{"job": "install", "package": package_a1},
],
)
+
+
+@pytest.mark.parametrize("with_extra", [False, True])
+def test_solver_resolves_duplicate_dependency_in_extra(
+ package: ProjectPackage,
+ pool: RepositoryPool,
+ repo: Repository,
+ io: NullIO,
+ with_extra: bool,
+) -> None:
+ """
+ Without extras, a newer version of B can be chosen than with extras.
+ See https://github.com/python-poetry/poetry/issues/8380.
+ """
+ constraint: dict[str, Any] = {"version": "*"}
+ if with_extra:
+ constraint["extras"] = ["foo"]
+ package.add_dependency(Factory.create_dependency("A", constraint))
+
+ package_a = get_package("A", "1.0")
+ package_b1 = get_package("B", "1.0")
+ package_b2 = get_package("B", "2.0")
+
+ dep = get_dependency("B", ">=1.0")
+ package_a.add_dependency(dep)
+
+ dep_extra = get_dependency("B", "^1.0", optional=True)
+ dep_extra.marker = parse_marker("extra == 'foo'")
+ package_a.extras = {canonicalize_name("foo"): [dep_extra]}
+ package_a.add_dependency(dep_extra)
+
+ repo.add_package(package_a)
+ repo.add_package(package_b1)
+ repo.add_package(package_b2)
+
+ solver = Solver(package, pool, [], [], io)
+ transaction = solver.solve()
+
+ check_solver_result(
+ transaction,
+ (
+ [
+ {"job": "install", "package": package_b1 if with_extra else package_b2},
+ {"job": "install", "package": package_a},
+ ]
+ ),
+ )
+
+
+def test_solver_resolves_duplicate_dependencies_with_restricted_extras(
+ package: ProjectPackage,
+ pool: RepositoryPool,
+ repo: Repository,
+ io: NullIO,
+) -> None:
+ package.add_dependency(
+ Factory.create_dependency("A", {"version": "*", "extras": ["foo"]})
+ )
+
+ package_a = get_package("A", "1.0")
+ package_b1 = get_package("B", "1.0")
+ package_b2 = get_package("B", "2.0")
+
+ dep1 = get_dependency("B", "^1.0", optional=True)
+ dep1.marker = parse_marker("sys_platform == 'win32' and extra == 'foo'")
+ dep2 = get_dependency("B", "^2.0", optional=True)
+ dep2.marker = parse_marker("sys_platform == 'linux' and extra == 'foo'")
+ package_a.extras = {canonicalize_name("foo"): [dep1, dep2]}
+ package_a.add_dependency(dep1)
+ package_a.add_dependency(dep2)
+
+ repo.add_package(package_a)
+ repo.add_package(package_b1)
+ repo.add_package(package_b2)
+
+ solver = Solver(package, pool, [], [], io)
+ transaction = solver.solve()
+
+ check_solver_result(
+ transaction,
+ (
+ [
+ {"job": "install", "package": package_b1},
+ {"job": "install", "package": package_b2},
+ {"job": "install", "package": package_a},
+ ]
+ ),
+ )