From 133db3cb7f0183cea65371ef87842d7b44fb36c9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 5 Jan 2021 03:26:28 +0800 Subject: [PATCH] Tree pruning seems to work? --- src/resolvelib/providers.py | 14 ++++++++++++++ src/resolvelib/resolvers.py | 32 +++++++++++++++++++++++++++++--- tests/test_resolvers.py | 8 ++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/resolvelib/providers.py b/src/resolvelib/providers.py index 965cf9c..d4969b7 100644 --- a/src/resolvelib/providers.py +++ b/src/resolvelib/providers.py @@ -94,6 +94,20 @@ def get_dependencies(self, candidate): """ raise NotImplementedError + def match_identically(self, requirements_a, requirements_b): + """Whether the two given requirement sets find the same candidates. + + This is used by the resolver to perform tree-pruning. If the two + requirement sets provide the same candidates, the resolver can avoid + visiting the subtree again when it's encountered, and directly mark it + as a dead end instead. + + Both arguments are iterators yielding requirement objects. A boolean + should be returned to indicate whether the two sets should be treated + as matching. + """ + raise NotImplementedError + class AbstractResolver(object): """The thing that performs the actual resolution work.""" diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 621267d..e9e4bda 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -1,4 +1,5 @@ import collections +import itertools from .providers import AbstractResolver from .structs import DirectedGraph, build_iter_view @@ -143,6 +144,7 @@ def __init__(self, provider, reporter): self._p = provider self._r = reporter self._states = [] + self._known_failures = [] @property def state(self): @@ -199,8 +201,29 @@ def _get_criteria_to_update(self, candidate): criteria[name] = crit return criteria + def _match_known_failure_causes(self, updating_criteria): + try: + match_identically = self._p.match_identically + except AttributeError: + return False + criteria = self.state.criteria.copy() + criteria.update(updating_criteria) + for state in self._known_failures: + identical = match_identically( + itertools.chain.from_iterable( + crit.iter_requirement() for crit in criteria.values() + ), + itertools.chain.from_iterable( + crit.iter_requirement() for crit in state.criteria.values() + ), + ) + if identical: + return True + return False + def _attempt_to_pin_criterion(self, name, criterion): causes = [] + for candidate in criterion.candidates: try: criteria = self._get_criteria_to_update(candidate) @@ -208,6 +231,9 @@ def _attempt_to_pin_criterion(self, name, criterion): causes.append(e.criterion) continue + if self._match_known_failure_causes(criteria): + continue + # Check the newly-pinned candidate actually works. This should # always pass under normal circumstances, but in the case of a # faulty provider, we will raise an error to notify the implementer @@ -226,7 +252,7 @@ def _attempt_to_pin_criterion(self, name, criterion): self.state.mapping[name] = candidate self.state.criteria.update(criteria) - return [] + return None # All candidates tried, nothing works. This criterion is a dead # end, signal for backtracking. @@ -260,7 +286,7 @@ def _backtrack(self): """ while len(self._states) >= 3: # Remove the state that triggered backtracking. - del self._states[-1] + self._known_failures.append(self._states.pop()) # Retrieve the last candidate pin and known incompatibilities. broken_state = self._states.pop() @@ -345,7 +371,7 @@ def resolve(self, requirements, max_rounds): ) failure_causes = self._attempt_to_pin_criterion(name, criterion) - if failure_causes: + if failure_causes is not None: # Backtrack if pinning fails. The backtrack process puts us in # an unpinned state, so we can work on it in the next round. success = self._backtrack() diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index 30eb07f..909bc6a 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -88,6 +88,14 @@ def is_satisfied_by(self, requirement, candidate): and candidate.version in requirement.versions ) + def match_identically(self, reqs1, reqs2): + vers1 = collections.defaultdict(set) + vers2 = collections.defaultdict(set) + for rs, vs in [(reqs1, vers1), (reqs2, vers2)]: + for r in rs: + vs[r.name] = vs[r.name].union(r.versions) + return vers1 == vers2 + def get_dependencies(self, candidate): return candidate.dependencies