@@ -75,6 +75,133 @@ def _get_with_identifier(
75
75
return default
76
76
77
77
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
+
78
205
class PipProvider (_ProviderBase ):
79
206
"""Pip's provider implementation for resolvelib.
80
207
@@ -253,3 +380,54 @@ def is_backtrack_cause(
253
380
if backtrack_cause .parent and identifier == backtrack_cause .parent .name :
254
381
return True
255
382
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