diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst new file mode 100644 index 00000000000..5e21f8b5295 --- /dev/null +++ b/news/resolvelib.vendor.rst @@ -0,0 +1 @@ +Upgrade resolvelib to 1.2.0 diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index c655c597c6f..41537e3e020 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -1,17 +1,17 @@ __all__ = [ - "__version__", "AbstractProvider", "AbstractResolver", "BaseReporter", "InconsistentCandidate", - "Resolver", "RequirementsConflicted", "ResolutionError", "ResolutionImpossible", "ResolutionTooDeep", + "Resolver", + "__version__", ] -__version__ = "1.1.0" +__version__ = "1.2.0" from .providers import AbstractProvider diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py index 26c9f6e6f92..6c142204b7d 100644 --- a/src/pip/_vendor/resolvelib/reporters.py +++ b/src/pip/_vendor/resolvelib/reporters.py @@ -9,7 +9,7 @@ class BaseReporter(Generic[RT, CT, KT]): - """Delegate class to provider progress reporting for the resolver.""" + """Delegate class to provide progress reporting for the resolver.""" def starting(self) -> None: """Called before the resolution actually starts.""" diff --git a/src/pip/_vendor/resolvelib/resolvers/__init__.py b/src/pip/_vendor/resolvelib/resolvers/__init__.py index 7b2c5d597eb..b24922152a3 100644 --- a/src/pip/_vendor/resolvelib/resolvers/__init__.py +++ b/src/pip/_vendor/resolvelib/resolvers/__init__.py @@ -13,15 +13,15 @@ __all__ = [ "AbstractResolver", + "Criterion", "InconsistentCandidate", - "Resolver", - "Resolution", + "RequirementInformation", "RequirementsConflicted", + "Resolution", "ResolutionError", "ResolutionImpossible", "ResolutionTooDeep", - "RequirementInformation", + "Resolver", "ResolverException", "Result", - "Criterion", ] diff --git a/src/pip/_vendor/resolvelib/resolvers/resolution.py b/src/pip/_vendor/resolvelib/resolvers/resolution.py index da3c66e2ab7..f55ac7ab928 100644 --- a/src/pip/_vendor/resolvelib/resolvers/resolution.py +++ b/src/pip/_vendor/resolvelib/resolvers/resolution.py @@ -3,7 +3,7 @@ import collections import itertools import operator -from typing import TYPE_CHECKING, Collection, Generic, Iterable, Mapping +from typing import TYPE_CHECKING, Generic from ..structs import ( CT, @@ -27,9 +27,13 @@ ) if TYPE_CHECKING: + from collections.abc import Collection, Iterable, Mapping + from ..providers import AbstractProvider, Preference from ..reporters import BaseReporter +_OPTIMISTIC_BACKJUMPING_RATIO: float = 0.1 + def _build_result(state: State[RT, CT, KT]) -> Result[RT, CT, KT]: mapping = state.mapping @@ -77,6 +81,11 @@ def __init__( self._r = reporter self._states: list[State[RT, CT, KT]] = [] + # Optimistic backjumping variables + self._optimistic_backjumping_ratio = _OPTIMISTIC_BACKJUMPING_RATIO + self._save_states: list[State[RT, CT, KT]] | None = None + self._optimistic_start_round: int | None = None + @property def state(self) -> State[RT, CT, KT]: try: @@ -274,6 +283,25 @@ def _patch_criteria( ) return True + def _save_state(self) -> None: + """Save states for potential rollback if optimistic backjumping fails.""" + if self._save_states is None: + self._save_states = [ + State( + mapping=s.mapping.copy(), + criteria=s.criteria.copy(), + backtrack_causes=s.backtrack_causes[:], + ) + for s in self._states + ] + + def _rollback_states(self) -> None: + """Rollback states and disable optimistic backjumping.""" + self._optimistic_backjumping_ratio = 0.0 + if self._save_states: + self._states = self._save_states + self._save_states = None + def _backjump(self, causes: list[RequirementInformation[RT, CT]]) -> bool: """Perform backjumping. @@ -324,13 +352,26 @@ def _backjump(self, causes: list[RequirementInformation[RT, CT]]) -> bool: except (IndexError, KeyError): raise ResolutionImpossible(causes) from None - # Only backjump if the current broken state is - # an incompatible dependency - if name not in incompatible_deps: + if ( + not self._optimistic_backjumping_ratio + and name not in incompatible_deps + ): + # For safe backjumping only backjump if the current dependency + # is not the same as the incompatible dependency break + # On the first time a non-safe backjump is done the state + # is saved so we can restore it later if the resolution fails + if ( + self._optimistic_backjumping_ratio + and self._save_states is None + and name not in incompatible_deps + ): + self._save_state() + # If the current dependencies and the incompatible dependencies - # are overlapping then we have found a cause of the incompatibility + # are overlapping then we have likely found a cause of the + # incompatibility current_dependencies = { self._p.identify(d) for d in self._p.get_dependencies(candidate) } @@ -394,9 +435,32 @@ def resolve(self, requirements: Iterable[RT], max_rounds: int) -> State[RT, CT, # pinning the virtual "root" package in the graph. self._push_new_state() + # Variables for optimistic backjumping + optimistic_rounds_cutoff: int | None = None + optimistic_backjumping_start_round: int | None = None + for round_index in range(max_rounds): self._r.starting_round(index=round_index) + # Handle if optimistic backjumping has been running for too long + if self._optimistic_backjumping_ratio and self._save_states is not None: + if optimistic_backjumping_start_round is None: + optimistic_backjumping_start_round = round_index + optimistic_rounds_cutoff = int( + (max_rounds - round_index) * self._optimistic_backjumping_ratio + ) + + if optimistic_rounds_cutoff <= 0: + self._rollback_states() + continue + elif optimistic_rounds_cutoff is not None: + if ( + round_index - optimistic_backjumping_start_round + >= optimistic_rounds_cutoff + ): + self._rollback_states() + continue + unsatisfied_names = [ key for key, criterion in self.state.criteria.items() @@ -448,12 +512,29 @@ def resolve(self, requirements: Iterable[RT], max_rounds: int) -> State[RT, CT, # Backjump if pinning fails. The backjump 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._backjump(causes) - self.state.backtrack_causes[:] = causes - # Dead ends everywhere. Give up. - if not success: - raise ResolutionImpossible(self.state.backtrack_causes) + try: + success = self._backjump(causes) + except ResolutionImpossible: + if self._optimistic_backjumping_ratio and self._save_states: + failed_optimistic_backjumping = True + else: + raise + else: + failed_optimistic_backjumping = bool( + not success + and self._optimistic_backjumping_ratio + and self._save_states + ) + + if failed_optimistic_backjumping and self._save_states: + self._rollback_states() + else: + self.state.backtrack_causes[:] = causes + + # Dead ends everywhere. Give up. + if not success: + raise ResolutionImpossible(self.state.backtrack_causes) else: # discard as information sources any invalidated names # (unsatisfied names that were previously satisfied) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 2073a1fe955..1d1644201f4 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ requests==2.32.4 rich==14.0.0 pygments==2.19.2 typing_extensions==4.14.0 -resolvelib==1.1.0 +resolvelib==1.2.0 setuptools==70.3.0 tomli==2.2.1 tomli-w==1.2.0