Skip to content

Commit

Permalink
Mark downstream incompatibilities on backtracking
Browse files Browse the repository at this point in the history
Consider this situation:

* Candidate Ax depends on B
* Candidate B1 depends on C==1, D==1, E==1
* Candidate B2 depends on C==2, D==1, E==1
* Candidate C1 depends on D==1
* Candidate C2 depends on D==1
* Candidate D1 depends on E!=1

In the previous implementation, the conflict on E is discovered after we
resolved Ax-B1-C1-D1. D1 is marked as an incompatibility, we backtrack
the C1 pin. But now we don't have an available C, and need to also
backtrack the B1 pin. At this point, however, the previous implemen-
tation would fail to "remember" that D1 is also marked as incompatible,
and proceed to try B2. That would eventually fail, we backtrack to the
same point, are stuck trying B1 and B2 repeatedly.

This fix uses a list to remember all the candidates marked as
incompatible along the whole backtrack process, and "re-mark" them in
parent states. This makes the resolver aware, when it backtracks B1,
that B2 is also not viable, and avoid hitting it.
  • Loading branch information
uranusjr committed Nov 4, 2020
1 parent 1a302b6 commit 1521ccb
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 20 deletions.
36 changes: 24 additions & 12 deletions src/resolvelib/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/resolvelib/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down
7 changes: 4 additions & 3 deletions tests/test_structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit 1521ccb

Please sign in to comment.