Skip to content

Commit 76f34f7

Browse files
committed
Prefer conflicting causes
1 parent 954075f commit 76f34f7

File tree

1 file changed

+102
-1
lines changed

1 file changed

+102
-1
lines changed

src/pip/_internal/resolution/resolvelib/provider.py

+102-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,100 @@ def _get_with_identifier(
7575
return default
7676

7777

78+
def conflicting_causes(
79+
causes: Sequence["PreferenceInformation"],
80+
) -> Sequence["PreferenceInformation"]:
81+
"""Given causes return which causes conflict with each other
82+
For each cause check one of two things:
83+
1. If it's specifier conflicts with another causes parent version
84+
2. If it's specifier conflicts with another causes specifier
85+
86+
Any causes which match this criteria are returned as conflicting causes
87+
"""
88+
conflicting_ids: set[int] = set()
89+
90+
# Build a relationship between causes, cause ids, and cause parent names
91+
causes_id_and_parents_by_name: dict[
92+
str, list[tuple[int, Candidate]]
93+
] = collections.defaultdict(list)
94+
causes_by_id = {id(c): c for c in causes}
95+
for cause_id, cause in causes_by_id.items():
96+
if cause.parent:
97+
causes_id_and_parents_by_name[cause.parent.name].append(
98+
(cause_id, cause.parent)
99+
)
100+
101+
# From 1, check if each cause's specifier conflicts
102+
# with another causes parent's version
103+
for cause_id, cause in causes_by_id.items():
104+
if cause_id in conflicting_ids:
105+
continue
106+
107+
cause_id_and_parents = causes_id_and_parents_by_name.get(cause.requirement.name)
108+
if not cause_id_and_parents:
109+
continue
110+
111+
conflicting_alternative_cause_ids: set[int] = set()
112+
for alternative_cause_id, parent in cause_id_and_parents:
113+
if not cause.requirement.is_satisfied_by(parent):
114+
conflicting_alternative_cause_ids.add(alternative_cause_id)
115+
116+
if conflicting_alternative_cause_ids:
117+
conflicting_ids.add(cause_id)
118+
conflicting_ids.update(conflicting_alternative_cause_ids)
119+
120+
# For comparing if two specifiers conflict first group causes
121+
# by name, as comparing specifiers is O(n^2) so comparing the
122+
# smaller groups is more efficent
123+
causes_by_name: dict[str, list["PreferenceInformation"]] = collections.defaultdict(
124+
list
125+
)
126+
for cause in causes:
127+
causes_by_name[cause.requirement.name].append(cause)
128+
129+
# From 2, check if each cause's specifier conflicts
130+
# with another cause specifier
131+
for causes_list in causes_by_name.values():
132+
if len(causes_list) < 2:
133+
continue
134+
135+
while causes_list:
136+
cause = causes_list.pop()
137+
for i, alternative_cause in enumerate(causes_list):
138+
candidate = cause.requirement.get_candidate_lookup()[1]
139+
if candidate is None:
140+
continue
141+
specifier = candidate.specifier
142+
143+
# Specifiers which provide no restrictions can be skipped
144+
if len(specifier) == 0:
145+
continue
146+
147+
alternative_candidate = (
148+
alternative_cause.requirement.get_candidate_lookup()[1]
149+
)
150+
if alternative_candidate is None:
151+
continue
152+
153+
alternative_specifier = alternative_candidate.specifier
154+
155+
# Alternative specifiers which provide no
156+
# restrictions can be skipped
157+
if len(alternative_specifier) == 0:
158+
continue
159+
160+
# If intersection of specifiers are empty they are
161+
# impossibe to fill and therefore conflicting
162+
specifier_intersection = specifier and alternative_specifier
163+
if len(specifier_intersection) == 0:
164+
conflicting_ids.add(id(cause))
165+
conflicting_ids.add(id(causes_list.pop(i)))
166+
167+
return [
168+
cause for cause_id, cause in causes_by_id.items() if cause_id in conflicting_ids
169+
]
170+
171+
78172
class PipProvider(_ProviderBase):
79173
"""Pip's provider implementation for resolvelib.
80174
@@ -243,11 +337,18 @@ def filter_unsatisfied_names(
243337
causes: Sequence["PreferenceInformation"],
244338
) -> Iterable[str]:
245339
"""
246-
Prefer backtracking on unsatisfied names that are causes
340+
Prefer backtracking on unsatisfied names that are conficting
341+
causes, or secondly are causes
247342
"""
248343
if not causes:
249344
return unsatisfied_names
250345

346+
# Check if backtrack causes are conflicting and prefer them
347+
if len(causes) > 2:
348+
_conflicting_causes = conflicting_causes(causes)
349+
if len(_conflicting_causes) > 1:
350+
causes = _conflicting_causes
351+
251352
# Extract the causes and parents names
252353
causes_names = set()
253354
for cause in causes:

0 commit comments

Comments
 (0)