diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 976608b..e5da798 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -99,16 +99,16 @@ def merged_with(self, provider, requirement, parent): raise RequirementsConflicted(criterion) return criterion - def excluded_of(self, candidate): - """Build a new instance from this, but excluding specified candidate. + def excluded_of(self, incompatibilities): + """Build a new instance from this, but excluding specified candidates. Returns the new instance, or None if we still have no valid candidates. """ - cands = self.candidates.excluding(candidate) + cands = self.candidates.excluding(incompatibilities) if not cands: return None incompats = list(self.incompatibilities) - incompats.append(candidate) + incompats.extend(incompatibilities) return type(self)(cands, list(self.information), incompats) @@ -238,10 +238,25 @@ def _attempt_to_pin_criterion(self, name, criterion): # end, signal for backtracking. return causes + def _mark_backtrack_on_state(self, incompatibilities): + state = self.state + for name, candidates in incompatibilities.items(): + try: + criterion = state.criteria[name] + except KeyError: + continue + criterion = criterion.excluded_of(candidates) + if criterion is None: + return False + state.criteria[name] = criterion + return True + def _backtrack(self): # Drop the current state, it's known not to work. del self._states[-1] + incompatibilities = collections.defaultdict(list) + # We need at least 2 states here: # (a) One to backtrack to. # (b) One to restore state (a) to its state prior to candidate-pinning, @@ -255,19 +270,16 @@ def _backtrack(self): except KeyError: continue self._r.backtracking(candidate) + incompatibilities[name].append(candidate) + + # Mark candidates identified during backtracking as incompatible. + if not self._mark_backtrack_on_state(incompatibilities): + continue # Create a new state to work on, with the newly known not-working # candidate excluded. self._push_new_state() - # Mark the retracted candidate as incompatible. - criterion = self.state.criteria[name].excluded_of(candidate) - if criterion is None: - # This state still does not work. Try the still previous state. - del self._states[-1] - continue - self.state.criteria[name] = criterion - return True return False diff --git a/src/resolvelib/structs.py b/src/resolvelib/structs.py index 479aad5..bb567ab 100644 --- a/src/resolvelib/structs.py +++ b/src/resolvelib/structs.py @@ -95,11 +95,11 @@ def for_preference(self): """Provide an candidate iterable for `get_preference()`""" return self._factory() - def excluding(self, candidate): - """Create a new `Candidates` instance excluding `candidate`.""" + def excluding(self, candidates): + """Create a new `Candidates` instance excluding `candidates`.""" def factory(): - return (c for c in self._factory() if c != candidate) + return (c for c in self._factory() if c not in candidates) return type(self)(factory) @@ -129,9 +129,9 @@ def for_preference(self): """Provide an candidate iterable for `get_preference()`""" return self._sequence - def excluding(self, candidate): + def excluding(self, candidates): """Create a new instance excluding `candidate`.""" - return type(self)([c for c in self._sequence if c != candidate]) + return type(self)([c for c in self._sequence if c not in candidates]) def build_iter_view(matches): diff --git a/tests/test_structs.py b/tests/test_structs.py index 39602b4..a920a1e 100644 --- a/tests/test_structs.py +++ b/tests/test_structs.py @@ -72,6 +72,7 @@ def test_iter_view_for_preference_based_on_sequence(source): def test_itera_view_excluding(source): view = build_iter_view(source) - assert list(view.excluding(0)) == [1] - assert list(view.excluding(1)) == [0] - assert list(view.excluding(2)) == [0, 1] + assert list(view.excluding([0])) == [1] + assert list(view.excluding([2])) == [0, 1] + assert list(view.excluding([0, 1])) == [] + assert list(view.excluding([1, 2])) == [0]