Skip to content

Commit abceccb

Browse files
committed
Update
1 parent bbd1015 commit abceccb

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed

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

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,133 @@ def _get_with_identifier(
7575
return default
7676

7777

78+
def _extract_names_from_causes_and_parents(
79+
causes: Iterable["PreferenceInformation"],
80+
) -> set[str]:
81+
"""
82+
Utility function to extract names from the causes and their parent packages
83+
84+
:params causes: An iterable of PreferenceInformation
85+
86+
Returns a set of strings, each representing the name of a requirement or
87+
its parent package that was in causes
88+
"""
89+
causes_names = set()
90+
for cause in causes:
91+
causes_names.add(cause.requirement.name)
92+
if cause.parent:
93+
causes_names.add(cause.parent.name)
94+
95+
return causes_names
96+
97+
98+
def _causes_with_conflicting_parent(
99+
causes: Iterable["PreferenceInformation"],
100+
) -> list["PreferenceInformation"]:
101+
"""
102+
Identifies causes that conflict because their parent package requirements
103+
are not satisfied by another cause, or vice versa.
104+
105+
:params causes: An iterable sequence of PreferenceInformation
106+
107+
Returns a list of PreferenceInformation objects that represent the causes
108+
where their parent conflicts
109+
"""
110+
# Avoid duplication by keeping track of already identified conflicting
111+
# causes by their id
112+
conflicting_causes_by_id: dict[int, "PreferenceInformation"] = {}
113+
all_causes_by_id = {id(c): c for c in causes}
114+
115+
# Map cause IDs and parent packages by parent name for quick lookup
116+
causes_ids_and_parents_by_parent_name: dict[
117+
str, list[tuple[int, Candidate]]
118+
] = collections.defaultdict(list)
119+
for cause_id, cause in all_causes_by_id.items():
120+
if cause.parent:
121+
causes_ids_and_parents_by_parent_name[cause.parent.name].append(
122+
(cause_id, cause.parent)
123+
)
124+
125+
# Identify a cause's requirement conflicts with another cause's parent
126+
for cause_id, cause in all_causes_by_id.items():
127+
if cause_id in conflicting_causes_by_id:
128+
continue
129+
130+
cause_id_and_parents = causes_ids_and_parents_by_parent_name.get(
131+
cause.requirement.name
132+
)
133+
if not cause_id_and_parents:
134+
continue
135+
136+
for other_cause_id, parent in cause_id_and_parents:
137+
if not cause.requirement.is_satisfied_by(parent):
138+
conflicting_causes_by_id[cause_id] = cause
139+
conflicting_causes_by_id[other_cause_id] = all_causes_by_id[
140+
other_cause_id
141+
]
142+
143+
return list(conflicting_causes_by_id.values())
144+
145+
146+
def _first_causes_with_no_candidates(
147+
causes: Sequence["PreferenceInformation"],
148+
candidates: Mapping[str, Iterator[Candidate]],
149+
) -> list["PreferenceInformation"]:
150+
"""
151+
Checks for causes that have no possible candidates to satisfy their
152+
requirements. Returns first causes found as iterating candidates can
153+
be expensive due to downloading and building packages.
154+
155+
:params causes: A sequence of PreferenceInformation
156+
:params candidates: A mapping of package names to iterators of their candidates
157+
158+
Returns a list containing the first pair of PreferenceInformation objects
159+
that were found which had no satisfying candidates, else if all causes
160+
had at least some satisfying candidate an empty list is returned.
161+
"""
162+
# Group causes by package name to reduce the comparison complexity.
163+
causes_by_name: dict[str, list["PreferenceInformation"]] = collections.defaultdict(
164+
list
165+
)
166+
for cause in causes:
167+
causes_by_name[cause.requirement.project_name].append(cause)
168+
169+
# Check for cause pairs within the same package that have incompatible specifiers.
170+
for cause_name, causes_list in causes_by_name.items():
171+
if len(causes_list) < 2:
172+
continue
173+
174+
while causes_list:
175+
cause = causes_list.pop()
176+
candidate = cause.requirement.get_candidate_lookup()[1]
177+
if candidate is None:
178+
continue
179+
180+
for other_cause in causes_list:
181+
other_candidate = other_cause.requirement.get_candidate_lookup()[1]
182+
if other_candidate is None:
183+
continue
184+
185+
# Check if no candidate can match the combined specifier
186+
combined_specifier = candidate.specifier & other_candidate.specifier
187+
possible_candidates = candidates.get(cause_name)
188+
189+
# If no candidates have been provided then by default the
190+
# causes have no candidates
191+
if possible_candidates is None:
192+
return [cause, other_cause]
193+
194+
# Use any and contains version here instead of filter so
195+
# if a version is found that matches it will short curcuit
196+
# iterating through possible candidates
197+
if not any(
198+
combined_specifier.contains(c.version) for c in possible_candidates
199+
):
200+
return [cause, other_cause]
201+
202+
return []
203+
204+
78205
class PipProvider(_ProviderBase):
79206
"""Pip's provider implementation for resolvelib.
80207
@@ -253,3 +380,54 @@ def is_backtrack_cause(
253380
if backtrack_cause.parent and identifier == backtrack_cause.parent.name:
254381
return True
255382
return False
383+
384+
def narrow_requirement_selection(
385+
self,
386+
identifiers: Iterable[str],
387+
resolutions: Mapping[str, Candidate],
388+
candidates: Mapping[str, Iterator[Candidate]],
389+
information: Mapping[str, Iterable["PreferenceInformation"]],
390+
backtrack_causes: Sequence["PreferenceInformation"],
391+
) -> Iterable[str]:
392+
"""
393+
Narrows down the selection of requirements to consider for the next
394+
resolution step.
395+
396+
This method uses principles of conflict-driven clause learning (CDCL)
397+
to focus on the closest conflicts first.
398+
399+
:params identifiers: Iterable of requirement names currently under
400+
consideration.
401+
:params resolutions: Current mapping of resolved package identifiers
402+
to their selected candidates.
403+
:params candidates: Mapping of each package's possible candidates.
404+
:params information: Mapping of requirement information for each package.
405+
:params backtrack_causes: Sequence of requirements, if non-empty,
406+
were the cause of the resolver backtracking
407+
408+
Returns:
409+
An iterable of requirement names that the resolver will use to
410+
limit the next step of resolution
411+
"""
412+
if len(backtrack_causes) < 2:
413+
return identifiers
414+
415+
# First, try to resolve direct causes based on conflicting parent packages
416+
direct_causes = _causes_with_conflicting_parent(backtrack_causes)
417+
if not direct_causes:
418+
# If no conflicting parent packages found try to find some causes
419+
# that share the same requirement name but no common candidate,
420+
# we take the first one of these as iterating through candidates
421+
# is potentially expensive
422+
direct_causes = _first_causes_with_no_candidates(
423+
backtrack_causes, candidates
424+
)
425+
if direct_causes:
426+
backtrack_causes = direct_causes
427+
428+
# Filter identifiers based on the narrowed down causes.
429+
unsatisfied_causes_names = set(
430+
identifiers
431+
) & _extract_names_from_causes_and_parents(backtrack_causes)
432+
433+
return unsatisfied_causes_names if unsatisfied_causes_names else identifiers

0 commit comments

Comments
 (0)