diff --git a/src/resolvelib/reporters.py b/src/resolvelib/reporters.py index 563489e..6695480 100644 --- a/src/resolvelib/reporters.py +++ b/src/resolvelib/reporters.py @@ -30,6 +30,12 @@ def adding_requirement(self, requirement, parent): requirements passed in from ``Resolver.resolve()``. """ + 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 backtracking(self, candidate): """Called when rejecting a candidate during backtracking.""" diff --git a/src/resolvelib/reporters.pyi b/src/resolvelib/reporters.pyi index 55e38ab..03d4f09 100644 --- a/src/resolvelib/reporters.pyi +++ b/src/resolvelib/reporters.pyi @@ -7,4 +7,5 @@ class BaseReporter: def ending(self, state: Any) -> Any: ... def adding_requirement(self, requirement: Any, parent: Any) -> Any: ... def backtracking(self, candidate: Any) -> Any: ... + def resolving_conflicts(self, causes: Any) -> Any: ... def pinning(self, candidate: Any) -> Any: ... diff --git a/src/resolvelib/resolvers.py b/src/resolvelib/resolvers.py index 4d6a924..787681b 100644 --- a/src/resolvelib/resolvers.py +++ b/src/resolvelib/resolvers.py @@ -373,12 +373,12 @@ def resolve(self, requirements, max_rounds): failure_causes = self._attempt_to_pin_criterion(name) if failure_causes: + causes = [i for c in failure_causes for i in c.information] # Backtrack if pinning fails. The backtrack process puts us in # an unpinned state, so we can work on it in the next round. + self._r.resolving_conflicts(causes=causes) success = self._backtrack() - self.state.backtrack_causes[:] = [ - i for c in failure_causes for i in c.information - ] + self.state.backtrack_causes[:] = causes # Dead ends everywhere. Give up. if not success: diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py index b662694..800b745 100644 --- a/tests/test_resolvers.py +++ b/tests/test_resolvers.py @@ -5,6 +5,7 @@ BaseReporter, InconsistentCandidate, Resolver, + ResolutionImpossible, ) @@ -91,3 +92,54 @@ def is_satisfied_by(self, requirement, candidate): assert set(result.mapping) == {"parent", "child"} assert result.mapping["child"] == ("child", "1", []) + + +def test_resolving_conflicts(): + all_candidates = { + "a": [("a", 1, [("q", {1})]), ("a", 2, [("q", {2})])], + "b": [("b", 1, [("q", {1})])], + "q": [("q", 1, []), ("q", 2, [])], + } + + class Reporter(BaseReporter): + def __init__(self): + self.backtracking_causes = None + + def resolving_conflicts(self, causes): + self.backtracking_causes = causes + + class Provider(AbstractProvider): + def identify(self, requirement_or_candidate): + return requirement_or_candidate[0] + + def get_preference(self, **_): + return 0 + + def get_dependencies(self, candidate): + return candidate[2] + + def find_matches(self, identifier, requirements, incompatibilities): + bad_versions = {c[1] for c in incompatibilities[identifier]} + candidates = [ + c + for c in all_candidates[identifier] + if all(c[1] in r[1] for r in requirements[identifier]) + and c[1] not in bad_versions + ] + return sorted(candidates, key=lambda c: c[1], reverse=True) + + def is_satisfied_by(self, requirement, candidate): + return candidate[1] in requirement[1] + + def run_resolver(*args): + reporter = Reporter() + resolver = Resolver(Provider(), reporter) + try: + resolver.resolve(*args) + return reporter.backtracking_causes + except ResolutionImpossible as e: + return e.causes + + backtracking_causes = run_resolver([("a", {1, 2}), ("b", {1})]) + exception_causes = run_resolver([("a", {2}), ("b", {1})]) + assert exception_causes == backtracking_causes