Skip to content

Commit 9ad0cd3

Browse files
radoeringMrGreenTea
authored andcommitted
solver: ignore dependencies that are only relevant for inactive extras (python-poetry#8399)
1 parent caf6efe commit 9ad0cd3

File tree

2 files changed

+101
-5
lines changed

2 files changed

+101
-5
lines changed

src/poetry/puzzle/provider.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -606,10 +606,11 @@ def complete_package(
606606

607607
# For dependency resolution, markers of duplicate dependencies must be
608608
# mutually exclusive.
609-
deps = self._resolve_overlapping_markers(package, deps)
609+
active_extras = None if package.is_root() else dependency.extras
610+
deps = self._resolve_overlapping_markers(package, deps, active_extras)
610611

611612
if len(deps) == 1:
612-
self.debug(f"<debug>Merging requirements for {deps[0]!s}</debug>")
613+
self.debug(f"<debug>Merging requirements for {dep_name}</debug>")
613614
dependencies.append(deps[0])
614615
continue
615616

@@ -838,23 +839,30 @@ def _merge_dependencies_by_constraint(
838839

839840
return merged_dependencies
840841

841-
def _is_relevant_marker(self, marker: BaseMarker) -> bool:
842+
def _is_relevant_marker(
843+
self, marker: BaseMarker, active_extras: Collection[NormalizedName] | None
844+
) -> bool:
842845
"""
843846
A marker is relevant if
844847
- it is not empty
845848
- allowed by the project's python constraint
849+
- allowed by active extras of the dependency (not relevant for root package)
846850
- allowed by the environment (only during installation)
847851
"""
848852
return (
849853
not marker.is_empty()
850854
and self._python_constraint.allows_any(
851855
get_python_constraint_from_marker(marker)
852856
)
857+
and (active_extras is None or marker.validate({"extra": active_extras}))
853858
and (not self._env or marker.validate(self._env.marker_env))
854859
)
855860

856861
def _resolve_overlapping_markers(
857-
self, package: Package, dependencies: list[Dependency]
862+
self,
863+
package: Package,
864+
dependencies: list[Dependency],
865+
active_extras: Collection[NormalizedName] | None,
858866
) -> list[Dependency]:
859867
"""
860868
Convert duplicate dependencies with potentially overlapping markers
@@ -887,7 +895,7 @@ def _resolve_overlapping_markers(
887895
used_marker_intersection: BaseMarker = AnyMarker()
888896
for m in markers:
889897
used_marker_intersection = used_marker_intersection.intersect(m)
890-
if not self._is_relevant_marker(used_marker_intersection):
898+
if not self._is_relevant_marker(used_marker_intersection, active_extras):
891899
continue
892900

893901
# intersection of constraints

tests/puzzle/test_solver.py

+88
Original file line numberDiff line numberDiff line change
@@ -4322,3 +4322,91 @@ def test_update_with_use_latest_vs_lock(
43224322
{"job": "install", "package": package_a1},
43234323
],
43244324
)
4325+
4326+
4327+
@pytest.mark.parametrize("with_extra", [False, True])
4328+
def test_solver_resolves_duplicate_dependency_in_extra(
4329+
package: ProjectPackage,
4330+
pool: RepositoryPool,
4331+
repo: Repository,
4332+
io: NullIO,
4333+
with_extra: bool,
4334+
) -> None:
4335+
"""
4336+
Without extras, a newer version of B can be chosen than with extras.
4337+
See https://github.com/python-poetry/poetry/issues/8380.
4338+
"""
4339+
constraint: dict[str, Any] = {"version": "*"}
4340+
if with_extra:
4341+
constraint["extras"] = ["foo"]
4342+
package.add_dependency(Factory.create_dependency("A", constraint))
4343+
4344+
package_a = get_package("A", "1.0")
4345+
package_b1 = get_package("B", "1.0")
4346+
package_b2 = get_package("B", "2.0")
4347+
4348+
dep = get_dependency("B", ">=1.0")
4349+
package_a.add_dependency(dep)
4350+
4351+
dep_extra = get_dependency("B", "^1.0", optional=True)
4352+
dep_extra.marker = parse_marker("extra == 'foo'")
4353+
package_a.extras = {canonicalize_name("foo"): [dep_extra]}
4354+
package_a.add_dependency(dep_extra)
4355+
4356+
repo.add_package(package_a)
4357+
repo.add_package(package_b1)
4358+
repo.add_package(package_b2)
4359+
4360+
solver = Solver(package, pool, [], [], io)
4361+
transaction = solver.solve()
4362+
4363+
check_solver_result(
4364+
transaction,
4365+
(
4366+
[
4367+
{"job": "install", "package": package_b1 if with_extra else package_b2},
4368+
{"job": "install", "package": package_a},
4369+
]
4370+
),
4371+
)
4372+
4373+
4374+
def test_solver_resolves_duplicate_dependencies_with_restricted_extras(
4375+
package: ProjectPackage,
4376+
pool: RepositoryPool,
4377+
repo: Repository,
4378+
io: NullIO,
4379+
) -> None:
4380+
package.add_dependency(
4381+
Factory.create_dependency("A", {"version": "*", "extras": ["foo"]})
4382+
)
4383+
4384+
package_a = get_package("A", "1.0")
4385+
package_b1 = get_package("B", "1.0")
4386+
package_b2 = get_package("B", "2.0")
4387+
4388+
dep1 = get_dependency("B", "^1.0", optional=True)
4389+
dep1.marker = parse_marker("sys_platform == 'win32' and extra == 'foo'")
4390+
dep2 = get_dependency("B", "^2.0", optional=True)
4391+
dep2.marker = parse_marker("sys_platform == 'linux' and extra == 'foo'")
4392+
package_a.extras = {canonicalize_name("foo"): [dep1, dep2]}
4393+
package_a.add_dependency(dep1)
4394+
package_a.add_dependency(dep2)
4395+
4396+
repo.add_package(package_a)
4397+
repo.add_package(package_b1)
4398+
repo.add_package(package_b2)
4399+
4400+
solver = Solver(package, pool, [], [], io)
4401+
transaction = solver.solve()
4402+
4403+
check_solver_result(
4404+
transaction,
4405+
(
4406+
[
4407+
{"job": "install", "package": package_b1},
4408+
{"job": "install", "package": package_b2},
4409+
{"job": "install", "package": package_a},
4410+
]
4411+
),
4412+
)

0 commit comments

Comments
 (0)