diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index 527b8493fdc..0b668d1af39 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -645,9 +645,18 @@ def complete_package( active_extras = ( self._active_root_extras if package.is_root() else found_extras ) - deps = self._resolve_overlapping_markers(package, deps, active_extras) + deps = self._resolve_overlapping_markers( + package, + deps, + active_extras, + cover_leftover_marker_space=True, + ) else: - # There are duplicates with different extras. + # Resolve overlaps within each subgroup of duplicates that share + # the same extras. The leftover marker space (where no dep in a + # subgroup is required) is covered by the sibling subgroups, so + # _resolve_overlapping_markers must not synthesise an + # empty-constraint dep for it (see #10447). for complete_dep_name, deps_by_extra in duplicates_by_extras.items(): if len(deps_by_extra) > 1: duplicates_by_extras[complete_dep_name] = ( @@ -952,6 +961,8 @@ def _resolve_overlapping_markers( package: Package, dependencies: list[Dependency], active_extras: Collection[NormalizedName] | None, + *, + cover_leftover_marker_space: bool = False, ) -> list[Dependency]: """ Convert duplicate dependencies with potentially overlapping markers @@ -1009,6 +1020,10 @@ def _resolve_overlapping_markers( raise IncompatibleConstraintsError(package, *used_dependencies) if not any(uses): + if not cover_leftover_marker_space: + # Caller is responsible for the leftover marker space. + continue + # This is an edge case where the dependency is not required # for the resulting marker. However, we have to consider it anyway # in order to not miss other dependencies later, for instance: diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index 8a30dde71fc..a677143538b 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -5247,6 +5247,42 @@ def test_solver_resolves_duplicate_dependencies_with_restricted_extras( ) +def test_solver_resolves_optional_dependencies_and_group_with_extras( + package: ProjectPackage, + pool: RepositoryPool, + repo: Repository, + io: NullIO, +) -> None: + """Regression test for https://github.com/python-poetry/poetry/issues/10447.""" + dep_alchemy = get_dependency("A", "*", optional=True) + dep_alchemy._in_extras = [canonicalize_name("alchemy")] + dep_databases = get_dependency("A", "*", optional=True) + dep_databases._in_extras = [canonicalize_name("databases")] + package.extras = { + canonicalize_name("alchemy"): [dep_alchemy], + canonicalize_name("databases"): [dep_databases], + } + package.add_dependency(dep_alchemy) + package.add_dependency(dep_databases) + package.add_dependency( + Factory.create_dependency( + "A", {"version": "*", "extras": ["mypy"]}, groups=["test"] + ) + ) + + package_a = get_package("A", "1.0") + package_a.extras = {canonicalize_name("mypy"): []} + repo.add_package(package_a) + + solver = Solver(package, pool, [], [], io) + transaction = solver.solve() + + check_solver_result( + transaction, + [{"job": "install", "package": package_a}], + ) + + def test_solver_logs_age_filtered_versions_on_failure( mocker: MockerFixture, pool: RepositoryPool, solver: Solver ) -> None: