Skip to content

Commit d752ad3

Browse files
committed
Tree pruning seems to work?
1 parent 3cd67b0 commit d752ad3

File tree

3 files changed

+47
-3
lines changed

3 files changed

+47
-3
lines changed

src/resolvelib/providers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,21 @@ def get_dependencies(self, candidate):
9494
"""
9595
raise NotImplementedError
9696

97+
def match_identically(self, requirements_a, requirements_b):
98+
"""Whether the two given requirement sets find the same candidates.
99+
100+
This is used by the resolver to perform tree-pruning. If the two
101+
requirement sets provide the same candidates, the resolver can avoid
102+
visiting the subtree again when it's encountered, and directly mark it
103+
as a dead end instead.
104+
105+
Both arguments are iterators yielding requirement objects. A boolean
106+
should be returned to indicate whether the two sets should be treated
107+
as matching.
108+
"""
109+
return False # TODO: Remove this and implement the method in tests.
110+
raise NotImplementedError
111+
97112

98113
class AbstractResolver(object):
99114
"""The thing that performs the actual resolution work."""

src/resolvelib/resolvers.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import collections
2+
import itertools
23

34
from .providers import AbstractResolver
45
from .structs import DirectedGraph, build_iter_view
@@ -143,6 +144,7 @@ def __init__(self, provider, reporter):
143144
self._p = provider
144145
self._r = reporter
145146
self._states = []
147+
self._known_failures = []
146148

147149
@property
148150
def state(self):
@@ -199,6 +201,22 @@ def _get_criteria_to_update(self, candidate):
199201
criteria[name] = crit
200202
return criteria
201203

204+
def _match_known_failure_causes(self, updating_criteria):
205+
criteria = self.state.criteria.copy()
206+
criteria.update(updating_criteria)
207+
for state in self._known_failures:
208+
identical = self._p.match_identically(
209+
itertools.chain.from_iterable(
210+
crit.iter_requirement() for crit in criteria.values()
211+
),
212+
itertools.chain.from_iterable(
213+
crit.iter_requirement() for crit in state.criteria.values()
214+
),
215+
)
216+
if identical:
217+
return True
218+
return False
219+
202220
def _attempt_to_pin_criterion(self, name, criterion):
203221
causes = []
204222
for candidate in criterion.candidates:
@@ -208,6 +226,9 @@ def _attempt_to_pin_criterion(self, name, criterion):
208226
causes.append(e.criterion)
209227
continue
210228

229+
if self._match_known_failure_causes(criteria):
230+
continue
231+
211232
# Check the newly-pinned candidate actually works. This should
212233
# always pass under normal circumstances, but in the case of a
213234
# faulty provider, we will raise an error to notify the implementer
@@ -226,7 +247,7 @@ def _attempt_to_pin_criterion(self, name, criterion):
226247
self.state.mapping[name] = candidate
227248
self.state.criteria.update(criteria)
228249

229-
return []
250+
return None
230251

231252
# All candidates tried, nothing works. This criterion is a dead
232253
# end, signal for backtracking.
@@ -260,7 +281,7 @@ def _backtrack(self):
260281
"""
261282
while len(self._states) >= 3:
262283
# Remove the state that triggered backtracking.
263-
del self._states[-1]
284+
self._known_failures.append(self._states.pop())
264285

265286
# Retrieve the last candidate pin and known incompatibilities.
266287
broken_state = self._states.pop()
@@ -345,7 +366,7 @@ def resolve(self, requirements, max_rounds):
345366
)
346367
failure_causes = self._attempt_to_pin_criterion(name, criterion)
347368

348-
if failure_causes:
369+
if failure_causes is not None:
349370
# Backtrack if pinning fails. The backtrack process puts us in
350371
# an unpinned state, so we can work on it in the next round.
351372
success = self._backtrack()

tests/test_resolvers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ def is_satisfied_by(self, requirement, candidate):
8888
and candidate.version in requirement.versions
8989
)
9090

91+
def match_identically(self, reqs1, reqs2):
92+
vers1 = collections.defaultdict(set)
93+
vers2 = collections.defaultdict(set)
94+
for rs, vs in [(reqs1, vers1), (reqs2, vers2)]:
95+
for r in rs:
96+
vs[r.name] = vs[r.name].union(r.versions)
97+
return vers1 == vers2
98+
9199
def get_dependencies(self, candidate):
92100
return candidate.dependencies
93101

0 commit comments

Comments
 (0)