Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/resolvelib.vendor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Upgrade resolvelib to 1.2.0
6 changes: 3 additions & 3 deletions src/pip/_vendor/resolvelib/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_vendor/resolvelib/reporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
8 changes: 4 additions & 4 deletions src/pip/_vendor/resolvelib/resolvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@

__all__ = [
"AbstractResolver",
"Criterion",
"InconsistentCandidate",
"Resolver",
"Resolution",
"RequirementInformation",
"RequirementsConflicted",
"Resolution",
"ResolutionError",
"ResolutionImpossible",
"ResolutionTooDeep",
"RequirementInformation",
"Resolver",
"ResolverException",
"Result",
"Criterion",
]
101 changes: 91 additions & 10 deletions src/pip/_vendor/resolvelib/resolvers/resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_vendor/vendor.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down