Skip to content

Commit

Permalink
Allow provider to narrow backtrack selection
Browse files Browse the repository at this point in the history
  • Loading branch information
notatallshaw committed Feb 10, 2024
1 parent a062162 commit 1cf5bce
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 2 deletions.
10 changes: 10 additions & 0 deletions examples/extras_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,13 @@ def get_dependencies(self, candidate):
req = self.get_base_requirement(candidate)
deps.append(req)
return deps

def narrow_requirement_selection(
self,
identifiers,
resolutions,
candidates,
information,
backtrack_causes,
):
return identifiers
10 changes: 10 additions & 0 deletions examples/reporter_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ def is_satisfied_by(self, requirement, candidate):
def get_dependencies(self, candidate):
return self.candidates[candidate]

def narrow_requirement_selection(
self,
identifiers,
resolutions,
candidates,
information,
backtrack_causes,
):
return identifiers


class Reporter(resolvelib.BaseReporter):
def starting(self):
Expand Down
57 changes: 57 additions & 0 deletions src/resolvelib/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,60 @@ def get_dependencies(self, candidate: CT) -> Iterable[RT]:
specifies as its dependencies.
"""
raise NotImplementedError

def narrow_requirement_selection(
self,
identifiers: Iterable[KT],
resolutions: Mapping[KT, CT],
candidates: Mapping[KT, Iterator[CT]],
information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]],
backtrack_causes: Sequence[RequirementInformation[RT, CT]],
) -> Iterable[KT]:
"""
Narrows the selection of requirements being considered during
resolution.
The requirement selection is defined as "The possible requirements
that will be resolved next." If a requirement isn't part of the returned
iterable, it will not be considered during the next step of resolution.
:param identifiers: An iterable of `identifier` as returned by
``identify()``. These identify all requirements currently being
considered.
:param resolutions: Mapping of candidates currently pinned by the
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 are
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 (depended on) the
requirement, or ``None`` to indicate a root requirement.
Must return a non-empty subset of `identifiers`, with the simplest
implementation being to return `identifiers` unchanged.
Can be used by the provider to optimize the dependency resolution
process. `get_preference` will only be called on the identifiers
returned. If there is only one identifier returned, then `get_preference`
won't be called at all.
Serving a similar purpose as `get_preference`, this method allows the
provider to guide resolvelib through the resolution process. It should
be used instead of `get_preference` when the provider needs to consider
multiple identifiers simultaneously, or when the provider wants to skip
checking all identifiers, e.g., because the checks are prohibitively
expensive.
Returns:
Iterable[KT]: A non-empty subset of `identifiers`.
"""
return identifiers
26 changes: 24 additions & 2 deletions src/resolvelib/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,30 @@ def resolve(
unsatisfied_names
)

# Choose the most preferred unpinned criterion to try.
name = min(unsatisfied_names, key=self._get_preference)
if len(unsatisfied_names) > 1:
unstatisfied_names = list(
self._p.narrow_requirement_selection(
identifiers=unsatisfied_names,
resolutions=self.state.mapping,
candidates=IteratorMapping(
self.state.criteria,
operator.attrgetter("candidates"),
),
information=IteratorMapping(
self.state.criteria,
operator.attrgetter("information"),
),
backtrack_causes=self.state.backtrack_causes,
)
)

# If there is only 1 unsatisfied name skip calling self._get_preference
if len(unstatisfied_names) > 1:
# Choose the most preferred unpinned criterion to try.
name = min(unstatisfied_names, key=self._get_preference)
else:
name = unstatisfied_names[0]

failure_criterion = self._attempt_to_pin_criterion(name)

if failure_criterion:
Expand Down
10 changes: 10 additions & 0 deletions tests/functional/cocoapods/test_resolvers_cocoapods.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ def is_satisfied_by(self, requirement, candidate):
def get_dependencies(self, candidate):
return candidate.deps

def narrow_requirement_selection(
self,
identifiers,
resolutions,
candidates,
information,
backtrack_causes,
):
return identifiers


XFAIL_CASES = {
# ResolveLib does not complain about cycles, so these will be different.
Expand Down
10 changes: 10 additions & 0 deletions tests/functional/python/test_resolvers_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ def _iter_dependencies(self, candidate):
def get_dependencies(self, candidate):
return list(self._iter_dependencies(candidate))

def narrow_requirement_selection(
self,
identifiers,
resolutions,
candidates,
information,
backtrack_causes,
):
return identifiers


INPUTS_DIR = os.path.abspath(os.path.join(__file__, "..", "inputs"))

Expand Down
10 changes: 10 additions & 0 deletions tests/functional/swift-package-manager/test_resolvers_swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ def _iter_dependencies(self, candidate):
def get_dependencies(self, candidate):
return list(self._iter_dependencies(candidate))

def narrow_requirement_selection(
self,
identifiers,
resolutions,
candidates,
information,
backtrack_causes,
):
return identifiers


@pytest.fixture(
params=[os.path.join(INPUTS_DIR, n) for n in INPUT_NAMES],
Expand Down
12 changes: 12 additions & 0 deletions tests/test_resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ def is_satisfied_by(self, requirement, candidate):
assert candidate is self.candidate
return False

def narrow_requirement_selection(self, identifiers, **_):
return identifiers

resolver = Resolver(Provider(requirement, candidate), BaseReporter())

with pytest.raises(InconsistentCandidate) as ctx:
Expand Down Expand Up @@ -104,6 +107,9 @@ def find_matches(self, identifier, requirements, incompatibilities):
def is_satisfied_by(self, requirement, candidate):
return candidate[1] in requirement[1]

def narrow_requirement_selection(self, identifiers, **_):
return identifiers

# Now when resolved, both requirements to child specified by parent should
# be pulled, and the resolver should choose v1, not v2 (happens if the
# v1-only requirement is dropped).
Expand Down Expand Up @@ -164,6 +170,9 @@ def find_matches(self, identifier, requirements, incompatibilities):
def is_satisfied_by(self, requirement, candidate):
return candidate.version in requirement.versions

def narrow_requirement_selection(self, identifiers, **_):
return identifiers

def run_resolver(*args):
reporter = Reporter()
resolver = Resolver(Provider(), reporter)
Expand Down Expand Up @@ -243,6 +252,9 @@ def is_satisfied_by(
) -> bool:
return candidate[1] in Requirement(requirement).specifier

def narrow_requirement_selection(self, identifiers, **_):
return identifiers

# patch Resolution._get_updated_criteria to collect rejected states
rejected_criteria: list[Criterion] = []
get_updated_criteria_orig = (
Expand Down

0 comments on commit 1cf5bce

Please sign in to comment.