-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle restricted dependencies as implicit multiple-constraints dependencies #6969
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -570,6 +570,9 @@ def complete_package( | |||||
continue | ||||||
self.search_for_direct_origin_dependency(dep) | ||||||
|
||||||
active_extras = None if package.is_root() else dependency.extras | ||||||
_dependencies = self._add_implicit_dependencies(_dependencies, active_extras) | ||||||
|
||||||
dependencies = self._get_dependencies_with_overrides( | ||||||
_dependencies, dependency_package | ||||||
) | ||||||
|
@@ -606,7 +609,6 @@ def complete_package( | |||||
|
||||||
# For dependency resolution, markers of duplicate dependencies must be | ||||||
# mutually exclusive. | ||||||
active_extras = None if package.is_root() else dependency.extras | ||||||
deps = self._resolve_overlapping_markers(package, deps, active_extras) | ||||||
|
||||||
if len(deps) == 1: | ||||||
|
@@ -856,6 +858,42 @@ def _is_relevant_marker( | |||||
and (not self._env or marker.validate(self._env.marker_env)) | ||||||
) | ||||||
|
||||||
def _add_implicit_dependencies( | ||||||
self, | ||||||
dependencies: Iterable[Dependency], | ||||||
active_extras: Collection[NormalizedName] | None, | ||||||
) -> list[Dependency]: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The return value here is used only for
Suggested change
With this, you can avoid creating the entire dependency list, e.g. using |
||||||
""" | ||||||
This handles an edge case where the dependency is not required for a subset | ||||||
of possible environments. We have to create such an implicit "not required" | ||||||
dependency in order to not miss other dependencies later. | ||||||
For instance: | ||||||
• foo (1.0) ; python == 3.7 | ||||||
• foo (2.0) ; python == 3.8 | ||||||
• bar (2.0) ; python == 3.8 | ||||||
• bar (3.0) ; python == 3.9 | ||||||
the last dependency would be missed without this, | ||||||
because the intersection with both foo dependencies is empty. | ||||||
|
||||||
A special case of this edge case is a restricted dependency with a single | ||||||
constraint, see https://github.com/python-poetry/poetry/issues/5506 | ||||||
for details. | ||||||
""" | ||||||
by_name: dict[str, list[Dependency]] = defaultdict(list) | ||||||
for dep in dependencies: | ||||||
by_name[dep.name].append(dep) | ||||||
for _name, deps in by_name.items(): | ||||||
marker = marker_union(*[d.marker for d in deps]) | ||||||
if marker.is_any(): | ||||||
continue | ||||||
Comment on lines
+886
to
+888
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is |
||||||
inverted_marker = marker.invert() | ||||||
if self._is_relevant_marker(inverted_marker, active_extras): | ||||||
# Set constraint to empty to mark dependency as "not required". | ||||||
inverted_marker_dep = deps[0].with_constraint(EmptyConstraint()) | ||||||
inverted_marker_dep.marker = inverted_marker | ||||||
deps.append(inverted_marker_dep) | ||||||
Comment on lines
+882
to
+894
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These loops could be merged if 1) you use |
||||||
return [dep for deps in by_name.values() for dep in deps] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
def _resolve_overlapping_markers( | ||||||
self, | ||||||
package: Package, | ||||||
|
@@ -878,7 +916,20 @@ def _resolve_overlapping_markers( | |||||
dependencies = self._merge_dependencies_by_constraint(dependencies) | ||||||
|
||||||
new_dependencies = [] | ||||||
|
||||||
# We have can sort out the implicit "not required" dependency determined | ||||||
# in _add_implicit_dependencies, because it does not overlap with | ||||||
# any other dependency for sure. | ||||||
for i, dep in enumerate(dependencies): | ||||||
if dep.constraint.is_empty(): | ||||||
new_dependencies.append(dependencies.pop(i)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The blacklist = set()
for dep in dependencies:
if dep.constraint.is_empty():
blacklist.add(dep)
break Then later on in This avoids the |
||||||
break | ||||||
|
||||||
for uses in itertools.product([True, False], repeat=len(dependencies)): | ||||||
if not any(uses): | ||||||
# handled by the implicit "not required" dependency | ||||||
continue | ||||||
|
||||||
# intersection of markers | ||||||
# For performance optimization, we don't just intersect all markers at once, | ||||||
# but intersect them one after the other to get empty markers early. | ||||||
|
@@ -917,21 +968,6 @@ def _resolve_overlapping_markers( | |||||
# conflict in overlapping area | ||||||
raise IncompatibleConstraintsError(package, *used_dependencies) | ||||||
|
||||||
if not any(uses): | ||||||
# 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: | ||||||
# • foo (1.0) ; python == 3.7 | ||||||
# • foo (2.0) ; python == 3.8 | ||||||
# • bar (2.0) ; python == 3.8 | ||||||
# • bar (3.0) ; python == 3.9 | ||||||
# the last dependency would be missed without this, | ||||||
# because the intersection with both foo dependencies is empty. | ||||||
|
||||||
# Set constraint to empty to mark dependency as "not required". | ||||||
constraint = EmptyConstraint() | ||||||
used_dependencies = dependencies | ||||||
|
||||||
# build new dependency with intersected constraint and marker | ||||||
# (and correct source) | ||||||
new_dep = ( | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2101,6 +2101,54 @@ def test_duplicate_path_dependencies_same_path( | |
check_solver_result(transaction, [{"job": "install", "package": demo1}]) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"marker1, marker2", | ||
[ | ||
("python_version < '3.7'", "python_version >= '3.7'"), | ||
("sys_platform == 'linux'", "sys_platform != 'linux'"), | ||
( | ||
"python_version < '3.7' and sys_platform == 'linux'", | ||
"python_version >= '3.7' and sys_platform == 'linux'", | ||
Comment on lines
+2107
to
+2111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think |
||
), | ||
], | ||
) | ||
def test_solver_restricted_dependencies_with_empty_marker_intersection( | ||
marker1: str, marker2: str, solver: Solver, repo: Repository, package: Package | ||
) -> None: | ||
package.add_dependency( | ||
Factory.create_dependency("A1", {"version": "1.0", "markers": marker1}) | ||
) | ||
package.add_dependency( | ||
Factory.create_dependency("A2", {"version": "1.0", "markers": marker2}) | ||
) | ||
|
||
package_a1 = get_package("A1", "1.0") | ||
package_a1.add_dependency(Factory.create_dependency("B", {"version": "<2.0"})) | ||
|
||
package_a2 = get_package("A2", "1.0") | ||
package_a2.add_dependency(Factory.create_dependency("B", {"version": ">=2.0"})) | ||
|
||
package_b10 = get_package("B", "1.0") | ||
package_b20 = get_package("B", "2.0") | ||
|
||
repo.add_package(package_a1) | ||
repo.add_package(package_a2) | ||
repo.add_package(package_b10) | ||
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_a1}, | ||
{"job": "install", "package": package_a2}, | ||
], | ||
) | ||
|
||
|
||
def test_solver_fails_if_dependency_name_does_not_match_package( | ||
solver: Solver, repo: Repository, package: ProjectPackage | ||
) -> None: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
_dependencies
is only used once, it's probably better to skip the variable assignment, by inlining in into the_add_implicit_dependencies
call