Skip to content

Commit 8672af0

Browse files
committed
Allow provider to narrow backtrack selection
1 parent 995ed67 commit 8672af0

File tree

8 files changed

+145
-1
lines changed

8 files changed

+145
-1
lines changed

examples/extras_provider.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,13 @@ def get_dependencies(self, candidate):
5252
req = self.get_base_requirement(candidate)
5353
deps.append(req)
5454
return deps
55+
56+
def narrow_requirement_selection(
57+
self,
58+
unsatisfied_names,
59+
resolutions,
60+
candidates,
61+
information,
62+
backtrack_causes,
63+
):
64+
return unsatisfied_names

examples/reporter_demo.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ def is_satisfied_by(self, requirement, candidate):
101101
def get_dependencies(self, candidate):
102102
return self.candidates[candidate]
103103

104+
def narrow_requirement_selection(
105+
self,
106+
unsatisfied_names,
107+
resolutions,
108+
candidates,
109+
information,
110+
backtrack_causes,
111+
):
112+
return unsatisfied_names
113+
104114

105115
class Reporter(resolvelib.BaseReporter):
106116
def starting(self):

src/resolvelib/providers.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,60 @@ def get_dependencies(self, candidate: CT) -> Iterable[RT]:
136136
specifies as its dependencies.
137137
"""
138138
raise NotImplementedError
139+
140+
def narrow_requirement_selection(
141+
self,
142+
identifiers: Iterable[KT],
143+
resolutions: Mapping[KT, CT],
144+
candidates: Mapping[KT, Iterator[CT]],
145+
information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]],
146+
backtrack_causes: Sequence[RequirementInformation[RT, CT]],
147+
) -> Iterable[KT]:
148+
"""
149+
Narrows the selection of requirements being considered during
150+
resolution.
151+
152+
The requirement selection is defined as "The possible requirements
153+
that will be resolved next." If a requirement isn't part of the returned
154+
iterable, it will not be considered during the next step of resolution.
155+
156+
:param identifiers: An iterable of `identifier` as returned by
157+
``identify()``. These identify all requirements currently being
158+
considered.
159+
:param resolutions: Mapping of candidates currently pinned by the
160+
resolver. Each key is an identifier, and the value is a candidate.
161+
The candidate may conflict with requirements from ``information``.
162+
:param candidates: Mapping of each dependency's possible candidates.
163+
Each value is an iterator of candidates.
164+
:param information: Mapping of requirement information of each package.
165+
Each value is an iterator of *requirement information*.
166+
:param backtrack_causes: Sequence of *requirement information* that are
167+
the requirements that caused the resolver to most recently
168+
backtrack.
169+
170+
A *requirement information* instance is a named tuple with two members:
171+
172+
* ``requirement`` specifies a requirement contributing to the current
173+
list of candidates.
174+
* ``parent`` specifies the candidate that provides (depended on) the
175+
requirement, or ``None`` to indicate a root requirement.
176+
177+
Must return a non-empty subset of `identifiers`, with the simplest
178+
implementation being to return `identifiers` unchanged.
179+
180+
Can be used by the provider to optimize the dependency resolution
181+
process. `get_preference` will only be called on the identifiers
182+
returned. If there is only one identifier returned, then `get_preference`
183+
won't be called at all.
184+
185+
Serving a similar purpose as `get_preference`, this method allows the
186+
provider to guide resolvelib through the resolution process. It should
187+
be used instead of `get_preference` when the provider needs to consider
188+
multiple identifiers simultaneously, or when the provider wants to skip
189+
checking all identifiers, e.g., because the checks are prohibitively
190+
expensive.
191+
192+
Returns:
193+
Iterable[KT]: A non-empty subset of `identifiers`.
194+
"""
195+
raise NotImplementedError

src/resolvelib/resolvers.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,33 @@ def resolve(
435435
unsatisfied_names
436436
)
437437

438+
if len(unsatisfied_names) > 1:
439+
filtered_unstatisfied_names = list(
440+
self._p.narrow_requirement_selection(
441+
unsatisfied_names=unsatisfied_names,
442+
resolutions=self.state.mapping,
443+
candidates=IteratorMapping(
444+
self.state.criteria,
445+
operator.attrgetter("candidates"),
446+
),
447+
information=IteratorMapping(
448+
self.state.criteria,
449+
operator.attrgetter("information"),
450+
),
451+
backtrack_causes=self.state.backtrack_causes,
452+
)
453+
)
454+
else:
455+
filtered_unstatisfied_names = unsatisfied_names
456+
438457
# Choose the most preferred unpinned criterion to try.
439-
name = min(unsatisfied_names, key=self._get_preference)
458+
if len(filtered_unstatisfied_names) > 1:
459+
name = min(
460+
filtered_unstatisfied_names, key=self._get_preference
461+
)
462+
else:
463+
name = filtered_unstatisfied_names[0]
464+
440465
failure_criterion = self._attempt_to_pin_criterion(name)
441466

442467
if failure_criterion:

tests/functional/cocoapods/test_resolvers_cocoapods.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@ def is_satisfied_by(self, requirement, candidate):
222222
def get_dependencies(self, candidate):
223223
return candidate.deps
224224

225+
def narrow_requirement_selection(
226+
self,
227+
unsatisfied_names,
228+
resolutions,
229+
candidates,
230+
information,
231+
backtrack_causes,
232+
):
233+
return unsatisfied_names
234+
225235

226236
XFAIL_CASES = {
227237
# ResolveLib does not complain about cycles, so these will be different.

tests/functional/python/test_resolvers_python.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,16 @@ def _iter_dependencies(self, candidate):
119119
def get_dependencies(self, candidate):
120120
return list(self._iter_dependencies(candidate))
121121

122+
def narrow_requirement_selection(
123+
self,
124+
unsatisfied_names,
125+
resolutions,
126+
candidates,
127+
information,
128+
backtrack_causes,
129+
):
130+
return unsatisfied_names
131+
122132

123133
INPUTS_DIR = os.path.abspath(os.path.join(__file__, "..", "inputs"))
124134

tests/functional/swift-package-manager/test_resolvers_swift.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ def _iter_dependencies(self, candidate):
132132
def get_dependencies(self, candidate):
133133
return list(self._iter_dependencies(candidate))
134134

135+
def narrow_requirement_selection(
136+
self,
137+
unsatisfied_names,
138+
resolutions,
139+
candidates,
140+
information,
141+
backtrack_causes,
142+
):
143+
return unsatisfied_names
144+
135145

136146
@pytest.fixture(
137147
params=[os.path.join(INPUTS_DIR, n) for n in INPUT_NAMES],

tests/test_resolvers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ def is_satisfied_by(self, requirement, candidate):
5858
assert candidate is self.candidate
5959
return False
6060

61+
def narrow_requirement_selection(self, unsatisfied_names, **_):
62+
return unsatisfied_names
63+
6164
resolver = Resolver(Provider(requirement, candidate), BaseReporter())
6265

6366
with pytest.raises(InconsistentCandidate) as ctx:
@@ -104,6 +107,9 @@ def find_matches(self, identifier, requirements, incompatibilities):
104107
def is_satisfied_by(self, requirement, candidate):
105108
return candidate[1] in requirement[1]
106109

110+
def narrow_requirement_selection(self, unsatisfied_names, **_):
111+
return unsatisfied_names
112+
107113
# Now when resolved, both requirements to child specified by parent should
108114
# be pulled, and the resolver should choose v1, not v2 (happens if the
109115
# v1-only requirement is dropped).
@@ -164,6 +170,9 @@ def find_matches(self, identifier, requirements, incompatibilities):
164170
def is_satisfied_by(self, requirement, candidate):
165171
return candidate.version in requirement.versions
166172

173+
def narrow_requirement_selection(self, unsatisfied_names, **_):
174+
return unsatisfied_names
175+
167176
def run_resolver(*args):
168177
reporter = Reporter()
169178
resolver = Resolver(Provider(), reporter)
@@ -243,6 +252,9 @@ def is_satisfied_by(
243252
) -> bool:
244253
return candidate[1] in Requirement(requirement).specifier
245254

255+
def narrow_requirement_selection(self, unsatisfied_names, **_):
256+
return unsatisfied_names
257+
246258
# patch Resolution._get_updated_criteria to collect rejected states
247259
rejected_criteria: list[Criterion] = []
248260
get_updated_criteria_orig = (

0 commit comments

Comments
 (0)