This commit is contained in:
Ambulance Clerc
2023-06-01 08:59:37 +02:00
parent 1fe8228d1b
commit 796746d175
346 changed files with 18799 additions and 44645 deletions

View File

@@ -11,7 +11,7 @@ __all__ = [
"ResolutionTooDeep",
]
__version__ = "0.7.1"
__version__ = "1.0.1"
from .providers import AbstractProvider, AbstractResolver
@@ -19,8 +19,8 @@ from .reporters import BaseReporter
from .resolvers import (
InconsistentCandidate,
RequirementsConflicted,
Resolver,
ResolutionError,
ResolutionImpossible,
ResolutionTooDeep,
Resolver,
)

View File

@@ -1,5 +1,5 @@
class AbstractProvider(object):
"""Delegate class to provide requirement interface for the resolver."""
"""Delegate class to provide the required interface for the resolver."""
def identify(self, requirement_or_candidate):
"""Given a requirement, return an identifier for it.
@@ -9,7 +9,14 @@ class AbstractProvider(object):
"""
raise NotImplementedError
def get_preference(self, identifier, resolutions, candidates, information):
def get_preference(
self,
identifier,
resolutions,
candidates,
information,
backtrack_causes,
):
"""Produce a sort key for given requirement based on preference.
The preference is defined as "I think this requirement should be
@@ -17,23 +24,25 @@ class AbstractProvider(object):
this group of arguments is.
:param identifier: An identifier as returned by ``identify()``. This
identifies the dependency matches of which should be returned.
identifies the dependency matches which should be returned.
:param resolutions: Mapping of candidates currently pinned by the
resolver. Each key is an identifier, and the value a candidate.
resolver. Each key is an identifier, and the value is a candidate.
The candidate may conflict with requirements from ``information``.
:param candidates: Mapping of each dependency's possible candidates.
Each value is an iterator of candidates.
:param information: Mapping of requirement information of each package.
Each value is an iterator of *requirement information*.
:param backtrack_causes: Sequence of requirement information that were
the requirements that caused the resolver to most recently backtrack.
A *requirement information* instance is a named tuple with two members:
* ``requirement`` specifies a requirement contributing to the current
list of candidates.
* ``parent`` specifies the candidate that provides (dependend on) the
* ``parent`` specifies the candidate that provides (depended on) the
requirement, or ``None`` to indicate a root requirement.
The preference could depend on a various of issues, including (not
The preference could depend on various issues, including (not
necessarily in this order):
* Is this package pinned in the current resolution result?
@@ -52,7 +61,7 @@ class AbstractProvider(object):
raise NotImplementedError
def find_matches(self, identifier, requirements, incompatibilities):
"""Find all possible candidates that satisfy given constraints.
"""Find all possible candidates that satisfy the given constraints.
:param identifier: An identifier as returned by ``identify()``. This
identifies the dependency matches of which should be returned.
@@ -83,7 +92,7 @@ class AbstractProvider(object):
def is_satisfied_by(self, requirement, candidate):
"""Whether the given requirement can be satisfied by a candidate.
The candidate is guarenteed to have been generated from the
The candidate is guaranteed to have been generated from the
requirement.
A boolean should be returned to indicate whether ``candidate`` is a

View File

@@ -30,7 +30,13 @@ class BaseReporter(object):
requirements passed in from ``Resolver.resolve()``.
"""
def backtracking(self, candidate):
def resolving_conflicts(self, causes):
"""Called when starting to attempt requirement conflict resolution.
:param causes: The information on the collision that caused the backtracking.
"""
def rejecting_candidate(self, criterion, candidate):
"""Called when rejecting a candidate during backtracking."""
def pinning(self, candidate):

View File

@@ -1,10 +1,10 @@
import collections
import itertools
import operator
from .providers import AbstractResolver
from .structs import DirectedGraph, IteratorMapping, build_iter_view
RequirementInformation = collections.namedtuple(
"RequirementInformation", ["requirement", "parent"]
)
@@ -99,7 +99,7 @@ class ResolutionTooDeep(ResolutionError):
# Resolution state in a round.
State = collections.namedtuple("State", "mapping criteria")
State = collections.namedtuple("State", "mapping criteria backtrack_causes")
class Resolution(object):
@@ -131,6 +131,7 @@ class Resolution(object):
state = State(
mapping=base.mapping.copy(),
criteria=base.criteria.copy(),
backtrack_causes=base.backtrack_causes[:],
)
self._states.append(state)
@@ -173,6 +174,31 @@ class Resolution(object):
raise RequirementsConflicted(criterion)
criteria[identifier] = criterion
def _remove_information_from_criteria(self, criteria, parents):
"""Remove information from parents of criteria.
Concretely, removes all values from each criterion's ``information``
field that have one of ``parents`` as provider of the requirement.
:param criteria: The criteria to update.
:param parents: Identifiers for which to remove information from all criteria.
"""
if not parents:
return
for key, criterion in criteria.items():
criteria[key] = Criterion(
criterion.candidates,
[
information
for information in criterion.information
if (
information.parent is None
or self._p.identify(information.parent) not in parents
)
],
criterion.incompatibilities,
)
def _get_preference(self, name):
return self._p.get_preference(
identifier=name,
@@ -185,6 +211,7 @@ class Resolution(object):
self.state.criteria,
operator.attrgetter("information"),
),
backtrack_causes=self.state.backtrack_causes,
)
def _is_current_pin_satisfying(self, name, criterion):
@@ -211,6 +238,7 @@ class Resolution(object):
try:
criteria = self._get_updated_criteria(candidate)
except RequirementsConflicted as e:
self._r.rejecting_candidate(e.criterion, candidate)
causes.append(e.criterion)
continue
@@ -239,8 +267,8 @@ class Resolution(object):
# end, signal for backtracking.
return causes
def _backtrack(self):
"""Perform backtracking.
def _backjump(self, causes):
"""Perform backjumping.
When we enter here, the stack is like this::
@@ -256,22 +284,46 @@ class Resolution(object):
Each iteration of the loop will:
1. Discard Z.
2. Discard Y but remember its incompatibility information gathered
1. Identify Z. The incompatibility is not always caused by the latest
state. For example, given three requirements A, B and C, with
dependencies A1, B1 and C1, where A1 and B1 are incompatible: the
last state might be related to C, so we want to discard the
previous state.
2. Discard Z.
3. Discard Y but remember its incompatibility information gathered
previously, and the failure we're dealing with right now.
3. Push a new state Y' based on X, and apply the incompatibility
4. Push a new state Y' based on X, and apply the incompatibility
information from Y to Y'.
4a. If this causes Y' to conflict, we need to backtrack again. Make Y'
5a. If this causes Y' to conflict, we need to backtrack again. Make Y'
the new Z and go back to step 2.
4b. If the incompatibilities apply cleanly, end backtracking.
5b. If the incompatibilities apply cleanly, end backtracking.
"""
incompatible_reqs = itertools.chain(
(c.parent for c in causes if c.parent is not None),
(c.requirement for c in causes),
)
incompatible_deps = {self._p.identify(r) for r in incompatible_reqs}
while len(self._states) >= 3:
# Remove the state that triggered backtracking.
del self._states[-1]
# Retrieve the last candidate pin and known incompatibilities.
broken_state = self._states.pop()
name, candidate = broken_state.mapping.popitem()
# Ensure to backtrack to a state that caused the incompatibility
incompatible_state = False
while not incompatible_state:
# Retrieve the last candidate pin and known incompatibilities.
try:
broken_state = self._states.pop()
name, candidate = broken_state.mapping.popitem()
except (IndexError, KeyError):
raise ResolutionImpossible(causes)
current_dependencies = {
self._p.identify(d)
for d in self._p.get_dependencies(candidate)
}
incompatible_state = not current_dependencies.isdisjoint(
incompatible_deps
)
incompatibilities_from_broken = [
(k, list(v.incompatibilities))
for k, v in broken_state.criteria.items()
@@ -280,8 +332,6 @@ class Resolution(object):
# Also mark the newly known incompatibility.
incompatibilities_from_broken.append((name, [candidate]))
self._r.backtracking(candidate=candidate)
# Create a new state from the last known-to-work one, and apply
# the previously gathered incompatibility information.
def _patch_criteria():
@@ -335,7 +385,13 @@ class Resolution(object):
self._r.starting()
# Initialize the root state.
self._states = [State(mapping=collections.OrderedDict(), criteria={})]
self._states = [
State(
mapping=collections.OrderedDict(),
criteria={},
backtrack_causes=[],
)
]
for r in requirements:
try:
self._add_to_criteria(self.state.criteria, r, parent=None)
@@ -361,20 +417,38 @@ class Resolution(object):
self._r.ending(state=self.state)
return self.state
# keep track of satisfied names to calculate diff after pinning
satisfied_names = set(self.state.criteria.keys()) - set(
unsatisfied_names
)
# Choose the most preferred unpinned criterion to try.
name = min(unsatisfied_names, key=self._get_preference)
failure_causes = self._attempt_to_pin_criterion(name)
if failure_causes:
# Backtrack if pinning fails. The backtrack process puts us in
causes = [i for c in failure_causes for i in c.information]
# Backjump if pinning fails. The backjump process puts us in
# an unpinned state, so we can work on it in the next round.
success = self._backtrack()
self._r.resolving_conflicts(causes=causes)
success = self._backjump(causes)
self.state.backtrack_causes[:] = causes
# Dead ends everywhere. Give up.
if not success:
causes = [i for c in failure_causes for i in c.information]
raise ResolutionImpossible(causes)
raise ResolutionImpossible(self.state.backtrack_causes)
else:
# discard as information sources any invalidated names
# (unsatisfied names that were previously satisfied)
newly_unsatisfied_names = {
key
for key, criterion in self.state.criteria.items()
if key in satisfied_names
and not self._is_current_pin_satisfying(key, criterion)
}
self._remove_information_from_criteria(
self.state.criteria, newly_unsatisfied_names
)
# Pinning was successful. Push a new state to do another pin.
self._push_new_state()

View File

@@ -117,13 +117,14 @@ class _FactoryIterableView(object):
def __init__(self, factory):
self._factory = factory
self._iterable = None
def __repr__(self):
return "{}({})".format(type(self).__name__, list(self._factory()))
return "{}({})".format(type(self).__name__, list(self))
def __bool__(self):
try:
next(self._factory())
next(iter(self))
except StopIteration:
return False
return True
@@ -131,7 +132,11 @@ class _FactoryIterableView(object):
__nonzero__ = __bool__ # XXX: Python 2.
def __iter__(self):
return self._factory()
iterable = (
self._factory() if self._iterable is None else self._iterable
)
self._iterable, current = itertools.tee(iterable)
return current
class _SequenceIterableView(object):