diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst new file mode 100644 index 00000000000..d5e12f5b945 --- /dev/null +++ b/news/resolvelib.vendor.rst @@ -0,0 +1 @@ +Upgrade resolvelib to 1.1.0 diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index dc6e2e12e1f..1fe11f53463 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -748,7 +748,7 @@ def get_installation_error( # The simplest case is when we have *one* cause that can't be # satisfied. We just report that case. if len(e.causes) == 1: - req, parent = e.causes[0] + req, parent = next(iter(e.causes)) if req.name not in constraints: return self._report_single_requirement_conflict(req, parent) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index fb0dd85f112..8aee30e3915 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,4 +1,3 @@ -import collections import math from functools import lru_cache from typing import ( @@ -100,7 +99,6 @@ def __init__( self._ignore_dependencies = ignore_dependencies self._upgrade_strategy = upgrade_strategy self._user_requested = user_requested - self._known_depths: Dict[str, float] = collections.defaultdict(lambda: math.inf) def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str: return requirement_or_candidate.name @@ -157,23 +155,6 @@ def get_preference( direct = candidate is not None pinned = any(op[:2] == "==" for op in operators) unfree = bool(operators) - - try: - requested_order: Union[int, float] = self._user_requested[identifier] - except KeyError: - requested_order = math.inf - if has_information: - parent_depths = ( - self._known_depths[parent.name] if parent is not None else 0.0 - for _, parent in information[identifier] - ) - inferred_depth = min(d for d in parent_depths) + 1.0 - else: - inferred_depth = math.inf - else: - inferred_depth = 1.0 - self._known_depths[identifier] = inferred_depth - requested_order = self._user_requested.get(identifier, math.inf) # Requires-Python has only one candidate and the check is basically @@ -190,7 +171,6 @@ def get_preference( not direct, not pinned, not backtrack_cause, - inferred_depth, requested_order, not unfree, identifier, diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 0594569d850..f8ad815fe9f 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -1,6 +1,6 @@ from collections import defaultdict from logging import getLogger -from typing import Any, DefaultDict +from typing import Any, DefaultDict, Optional from pip._vendor.resolvelib.reporters import BaseReporter @@ -9,7 +9,7 @@ logger = getLogger(__name__) -class PipReporter(BaseReporter): +class PipReporter(BaseReporter[Requirement, Candidate, str]): def __init__(self) -> None: self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int) @@ -55,7 +55,7 @@ def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None: logger.debug(msg) -class PipDebuggingReporter(BaseReporter): +class PipDebuggingReporter(BaseReporter[Requirement, Candidate, str]): """A reporter that does an info log for every event it sees.""" def starting(self) -> None: @@ -71,7 +71,9 @@ def ending_round(self, index: int, state: Any) -> None: def ending(self, state: Any) -> None: logger.info("Reporter.ending(%r)", state) - def adding_requirement(self, requirement: Requirement, parent: Candidate) -> None: + def adding_requirement( + self, requirement: Requirement, parent: Optional[Candidate] + ) -> None: logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent) def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None: diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index c12beef0b2a..e6d5f303740 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -82,7 +82,7 @@ def resolve( user_requested=collected.user_requested, ) if "PIP_RESOLVER_DEBUG" in os.environ: - reporter: BaseReporter = PipDebuggingReporter() + reporter: BaseReporter[Requirement, Candidate, str] = PipDebuggingReporter() else: reporter = PipReporter() resolver: RLResolver[Requirement, Candidate, str] = RLResolver( diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index d92acc7bedf..c655c597c6f 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,12 +11,13 @@ "ResolutionTooDeep", ] -__version__ = "1.0.1" +__version__ = "1.1.0" -from .providers import AbstractProvider, AbstractResolver +from .providers import AbstractProvider from .reporters import BaseReporter from .resolvers import ( + AbstractResolver, InconsistentCandidate, RequirementsConflicted, ResolutionError, diff --git a/src/pip/_vendor/resolvelib/__init__.pyi b/src/pip/_vendor/resolvelib/__init__.pyi deleted file mode 100644 index d64c52ced00..00000000000 --- a/src/pip/_vendor/resolvelib/__init__.pyi +++ /dev/null @@ -1,11 +0,0 @@ -__version__: str - -from .providers import AbstractProvider as AbstractProvider -from .providers import AbstractResolver as AbstractResolver -from .reporters import BaseReporter as BaseReporter -from .resolvers import InconsistentCandidate as InconsistentCandidate -from .resolvers import RequirementsConflicted as RequirementsConflicted -from .resolvers import ResolutionError as ResolutionError -from .resolvers import ResolutionImpossible as ResolutionImpossible -from .resolvers import ResolutionTooDeep as ResolutionTooDeep -from .resolvers import Resolver as Resolver diff --git a/src/pip/_vendor/resolvelib/compat/__init__.py b/src/pip/_vendor/resolvelib/compat/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/pip/_vendor/resolvelib/compat/collections_abc.py b/src/pip/_vendor/resolvelib/compat/collections_abc.py deleted file mode 100644 index 1becc5093c5..00000000000 --- a/src/pip/_vendor/resolvelib/compat/collections_abc.py +++ /dev/null @@ -1,6 +0,0 @@ -__all__ = ["Mapping", "Sequence"] - -try: - from collections.abc import Mapping, Sequence -except ImportError: - from collections import Mapping, Sequence diff --git a/src/pip/_vendor/resolvelib/compat/collections_abc.pyi b/src/pip/_vendor/resolvelib/compat/collections_abc.pyi deleted file mode 100644 index 2a088b19a93..00000000000 --- a/src/pip/_vendor/resolvelib/compat/collections_abc.pyi +++ /dev/null @@ -1 +0,0 @@ -from collections.abc import Mapping, Sequence diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py index e99d87ee75f..524e3d83272 100644 --- a/src/pip/_vendor/resolvelib/providers.py +++ b/src/pip/_vendor/resolvelib/providers.py @@ -1,30 +1,58 @@ -class AbstractProvider(object): +from __future__ import annotations + +from typing import ( + TYPE_CHECKING, + Generic, + Iterable, + Iterator, + Mapping, + Sequence, +) + +from .structs import CT, KT, RT, Matches, RequirementInformation + +if TYPE_CHECKING: + from typing import Any, Protocol + + class Preference(Protocol): + def __lt__(self, __other: Any) -> bool: ... + + +class AbstractProvider(Generic[RT, CT, KT]): """Delegate class to provide the required interface for the resolver.""" - def identify(self, requirement_or_candidate): - """Given a requirement, return an identifier for it. + def identify(self, requirement_or_candidate: RT | CT) -> KT: + """Given a requirement or candidate, return an identifier for it. - This is used to identify a requirement, e.g. whether two requirements - should have their specifier parts merged. + This is used to identify, e.g. whether two requirements + should have their specifier parts merged or a candidate matches a + requirement via ``find_matches()``. """ raise NotImplementedError def get_preference( self, - identifier, - resolutions, - candidates, - information, - backtrack_causes, - ): + identifier: KT, + resolutions: Mapping[KT, CT], + candidates: Mapping[KT, Iterator[CT]], + information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]], + backtrack_causes: Sequence[RequirementInformation[RT, CT]], + ) -> Preference: """Produce a sort key for given requirement based on preference. + As this is a sort key it will be called O(n) times per backtrack + step, where n is the number of `identifier`s, if you have a check + which is expensive in some sense. E.g. It needs to make O(n) checks + per call or takes significant wall clock time, consider using + `narrow_requirement_selection` to filter the `identifier`s, which + is applied before this sort key is called. + The preference is defined as "I think this requirement should be resolved first". The lower the return value is, the more preferred this group of arguments is. :param identifier: An identifier as returned by ``identify()``. This - identifies the dependency matches which should be returned. + identifies the requirement 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``. @@ -32,8 +60,9 @@ def get_preference( 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. + :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: @@ -60,15 +89,21 @@ def get_preference( """ raise NotImplementedError - def find_matches(self, identifier, requirements, incompatibilities): + def find_matches( + self, + identifier: KT, + requirements: Mapping[KT, Iterator[RT]], + incompatibilities: Mapping[KT, Iterator[CT]], + ) -> Matches[CT]: """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. + :param identifier: An identifier as returned by ``identify()``. All + candidates returned by this method should produce the same + identifier. :param requirements: A mapping of requirements that all returned candidates must satisfy. Each key is an identifier, and the value an iterator of requirements for that dependency. - :param incompatibilities: A mapping of known incompatibilities of + :param incompatibilities: A mapping of known incompatibile candidates of each dependency. Each key is an identifier, and the value an iterator of incompatibilities known to the resolver. All incompatibilities *must* be excluded from the return value. @@ -89,7 +124,7 @@ def find_matches(self, identifier, requirements, incompatibilities): """ raise NotImplementedError - def is_satisfied_by(self, requirement, candidate): + def is_satisfied_by(self, requirement: RT, candidate: CT) -> bool: """Whether the given requirement can be satisfied by a candidate. The candidate is guaranteed to have been generated from the @@ -100,7 +135,7 @@ def is_satisfied_by(self, requirement, candidate): """ raise NotImplementedError - def get_dependencies(self, candidate): + def get_dependencies(self, candidate: CT) -> Iterable[RT]: """Get dependencies of a candidate. This should return a collection of requirements that `candidate` @@ -108,26 +143,54 @@ def get_dependencies(self, candidate): """ 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]: + """ + An optional method to narrow the selection of requirements being + considered during resolution. This method is called O(1) time per + backtrack step. + + :param identifiers: An iterable of `identifiers` as returned by + ``identify()``. These identify all requirements currently being + considered. + :param resolutions: A mapping of candidates currently pinned by the + resolver. Each key is an identifier, and the value is a candidate + that may conflict with requirements from ``information``. + :param candidates: A mapping of each dependency's possible candidates. + Each value is an iterator of candidates. + :param information: A mapping of requirement information for each package. + Each value is an iterator of *requirement information*. + :param backtrack_causes: A sequence of *requirement information* that are + the requirements causing the resolver to most recently + backtrack. -class AbstractResolver(object): - """The thing that performs the actual resolution work.""" - - base_exception = Exception - - def __init__(self, provider, reporter): - self.provider = provider - self.reporter = reporter - - def resolve(self, requirements, **kwargs): - """Take a collection of constraints, spit out the resolution result. - - This returns a representation of the final resolution state, with one - guarenteed attribute ``mapping`` that contains resolved candidates as - values. The keys are their respective identifiers. - - :param requirements: A collection of constraints. - :param kwargs: Additional keyword arguments that subclasses may accept. + A *requirement information* instance is a named tuple with two members: - :raises: ``self.base_exception`` or its subclass. + * ``requirement`` specifies a requirement contributing to the current + list of candidates. + * ``parent`` specifies the candidate that provides (is depended on for) + the requirement, or ``None`` to indicate a root requirement. + + Must return a non-empty subset of `identifiers`, with the default + implementation being to return `identifiers` unchanged. Those `identifiers` + will then be passed to the sort key `get_preference` to pick the most + prefered requirement to attempt to pin, unless `narrow_requirement_selection` + returns only 1 requirement, in which case that will be used without + calling the sort key `get_preference`. + + This method is designed to be used by the provider to optimize the + dependency resolution, e.g. if a check cost is O(m) and it can be done + against all identifiers at once then filtering the requirement selection + here will cost O(m) but making it part of the sort key in `get_preference` + will cost O(m*n), where n is the number of `identifiers`. + + Returns: + Iterable[KT]: A non-empty subset of `identifiers`. """ - raise NotImplementedError + return identifiers diff --git a/src/pip/_vendor/resolvelib/providers.pyi b/src/pip/_vendor/resolvelib/providers.pyi deleted file mode 100644 index ec054194ee3..00000000000 --- a/src/pip/_vendor/resolvelib/providers.pyi +++ /dev/null @@ -1,44 +0,0 @@ -from typing import ( - Any, - Generic, - Iterable, - Iterator, - Mapping, - Protocol, - Sequence, - Union, -) - -from .reporters import BaseReporter -from .resolvers import RequirementInformation -from .structs import CT, KT, RT, Matches - -class Preference(Protocol): - def __lt__(self, __other: Any) -> bool: ... - -class AbstractProvider(Generic[RT, CT, KT]): - def identify(self, requirement_or_candidate: Union[RT, CT]) -> KT: ... - def get_preference( - self, - identifier: KT, - resolutions: Mapping[KT, CT], - candidates: Mapping[KT, Iterator[CT]], - information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]], - backtrack_causes: Sequence[RequirementInformation[RT, CT]], - ) -> Preference: ... - def find_matches( - self, - identifier: KT, - requirements: Mapping[KT, Iterator[RT]], - incompatibilities: Mapping[KT, Iterator[CT]], - ) -> Matches: ... - def is_satisfied_by(self, requirement: RT, candidate: CT) -> bool: ... - def get_dependencies(self, candidate: CT) -> Iterable[RT]: ... - -class AbstractResolver(Generic[RT, CT, KT]): - base_exception = Exception - provider: AbstractProvider[RT, CT, KT] - reporter: BaseReporter - def __init__( - self, provider: AbstractProvider[RT, CT, KT], reporter: BaseReporter - ): ... diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py index 688b5e10d86..26c9f6e6f92 100644 --- a/src/pip/_vendor/resolvelib/reporters.py +++ b/src/pip/_vendor/resolvelib/reporters.py @@ -1,26 +1,36 @@ -class BaseReporter(object): +from __future__ import annotations + +from typing import TYPE_CHECKING, Collection, Generic + +from .structs import CT, KT, RT, RequirementInformation, State + +if TYPE_CHECKING: + from .resolvers import Criterion + + +class BaseReporter(Generic[RT, CT, KT]): """Delegate class to provider progress reporting for the resolver.""" - def starting(self): + def starting(self) -> None: """Called before the resolution actually starts.""" - def starting_round(self, index): + def starting_round(self, index: int) -> None: """Called before each round of resolution starts. The index is zero-based. """ - def ending_round(self, index, state): + def ending_round(self, index: int, state: State[RT, CT, KT]) -> None: """Called before each round of resolution ends. This is NOT called if the resolution ends at this round. Use `ending` if you want to report finalization. The index is zero-based. """ - def ending(self, state): + def ending(self, state: State[RT, CT, KT]) -> None: """Called before the resolution ends successfully.""" - def adding_requirement(self, requirement, parent): + def adding_requirement(self, requirement: RT, parent: CT | None) -> None: """Called when adding a new requirement into the resolve criteria. :param requirement: The additional requirement to be applied to filter @@ -30,14 +40,16 @@ def adding_requirement(self, requirement, parent): requirements passed in from ``Resolver.resolve()``. """ - def resolving_conflicts(self, causes): + def resolving_conflicts( + self, causes: Collection[RequirementInformation[RT, CT]] + ) -> None: """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): + def rejecting_candidate(self, criterion: Criterion[RT, CT], candidate: CT) -> None: """Called when rejecting a candidate during backtracking.""" - def pinning(self, candidate): + def pinning(self, candidate: CT) -> None: """Called when adding a candidate to the potential solution.""" diff --git a/src/pip/_vendor/resolvelib/reporters.pyi b/src/pip/_vendor/resolvelib/reporters.pyi deleted file mode 100644 index b2ad286ba06..00000000000 --- a/src/pip/_vendor/resolvelib/reporters.pyi +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Any - -class BaseReporter: - def starting(self) -> Any: ... - def starting_round(self, index: int) -> Any: ... - def ending_round(self, index: int, state: Any) -> Any: ... - def ending(self, state: Any) -> Any: ... - def adding_requirement(self, requirement: Any, parent: Any) -> Any: ... - def rejecting_candidate(self, criterion: Any, candidate: Any) -> Any: ... - def resolving_conflicts(self, causes: Any) -> Any: ... - def pinning(self, candidate: Any) -> Any: ... diff --git a/src/pip/_vendor/resolvelib/resolvers.pyi b/src/pip/_vendor/resolvelib/resolvers.pyi deleted file mode 100644 index 528a1a259af..00000000000 --- a/src/pip/_vendor/resolvelib/resolvers.pyi +++ /dev/null @@ -1,79 +0,0 @@ -from typing import ( - Collection, - Generic, - Iterable, - Iterator, - List, - Mapping, - Optional, -) - -from .providers import AbstractProvider, AbstractResolver -from .structs import CT, KT, RT, DirectedGraph, IterableView - -# This should be a NamedTuple, but Python 3.6 has a bug that prevents it. -# https://stackoverflow.com/a/50531189/1376863 -class RequirementInformation(tuple, Generic[RT, CT]): - requirement: RT - parent: Optional[CT] - -class Criterion(Generic[RT, CT, KT]): - candidates: IterableView[CT] - information: Collection[RequirementInformation[RT, CT]] - incompatibilities: List[CT] - @classmethod - def from_requirement( - cls, - provider: AbstractProvider[RT, CT, KT], - requirement: RT, - parent: Optional[CT], - ) -> Criterion[RT, CT, KT]: ... - def iter_requirement(self) -> Iterator[RT]: ... - def iter_parent(self) -> Iterator[Optional[CT]]: ... - def merged_with( - self, - provider: AbstractProvider[RT, CT, KT], - requirement: RT, - parent: Optional[CT], - ) -> Criterion[RT, CT, KT]: ... - def excluded_of(self, candidates: List[CT]) -> Criterion[RT, CT, KT]: ... - -class ResolverException(Exception): ... - -class RequirementsConflicted(ResolverException, Generic[RT, CT, KT]): - criterion: Criterion[RT, CT, KT] - -class ResolutionError(ResolverException): ... - -class InconsistentCandidate(ResolverException, Generic[RT, CT, KT]): - candidate: CT - criterion: Criterion[RT, CT, KT] - -class ResolutionImpossible(ResolutionError, Generic[RT, CT]): - causes: List[RequirementInformation[RT, CT]] - -class ResolutionTooDeep(ResolutionError): - round_count: int - -# This should be a NamedTuple, but Python 3.6 has a bug that prevents it. -# https://stackoverflow.com/a/50531189/1376863 -class State(tuple, Generic[RT, CT, KT]): - mapping: Mapping[KT, CT] - criteria: Mapping[KT, Criterion[RT, CT, KT]] - backtrack_causes: Collection[RequirementInformation[RT, CT]] - -class Resolution(Generic[RT, CT, KT]): - def resolve( - self, requirements: Iterable[RT], max_rounds: int - ) -> State[RT, CT, KT]: ... - -class Result(Generic[RT, CT, KT]): - mapping: Mapping[KT, CT] - graph: DirectedGraph[Optional[KT]] - criteria: Mapping[KT, Criterion[RT, CT, KT]] - -class Resolver(AbstractResolver, Generic[RT, CT, KT]): - base_exception = ResolverException - def resolve( - self, requirements: Iterable[RT], max_rounds: int = 100 - ) -> Result[RT, CT, KT]: ... diff --git a/src/pip/_vendor/resolvelib/resolvers/__init__.py b/src/pip/_vendor/resolvelib/resolvers/__init__.py new file mode 100644 index 00000000000..7b2c5d597eb --- /dev/null +++ b/src/pip/_vendor/resolvelib/resolvers/__init__.py @@ -0,0 +1,27 @@ +from ..structs import RequirementInformation +from .abstract import AbstractResolver, Result +from .criterion import Criterion +from .exceptions import ( + InconsistentCandidate, + RequirementsConflicted, + ResolutionError, + ResolutionImpossible, + ResolutionTooDeep, + ResolverException, +) +from .resolution import Resolution, Resolver + +__all__ = [ + "AbstractResolver", + "InconsistentCandidate", + "Resolver", + "Resolution", + "RequirementsConflicted", + "ResolutionError", + "ResolutionImpossible", + "ResolutionTooDeep", + "RequirementInformation", + "ResolverException", + "Result", + "Criterion", +] diff --git a/src/pip/_vendor/resolvelib/resolvers/abstract.py b/src/pip/_vendor/resolvelib/resolvers/abstract.py new file mode 100644 index 00000000000..f9b5a7aa1fa --- /dev/null +++ b/src/pip/_vendor/resolvelib/resolvers/abstract.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import collections +from typing import TYPE_CHECKING, Any, Generic, Iterable, Mapping, NamedTuple + +from ..structs import CT, KT, RT, DirectedGraph + +if TYPE_CHECKING: + from ..providers import AbstractProvider + from ..reporters import BaseReporter + from .criterion import Criterion + + class Result(NamedTuple, Generic[RT, CT, KT]): + mapping: Mapping[KT, CT] + graph: DirectedGraph[KT | None] + criteria: Mapping[KT, Criterion[RT, CT]] + +else: + Result = collections.namedtuple("Result", ["mapping", "graph", "criteria"]) + + +class AbstractResolver(Generic[RT, CT, KT]): + """The thing that performs the actual resolution work.""" + + base_exception = Exception + + def __init__( + self, + provider: AbstractProvider[RT, CT, KT], + reporter: BaseReporter[RT, CT, KT], + ) -> None: + self.provider = provider + self.reporter = reporter + + def resolve(self, requirements: Iterable[RT], **kwargs: Any) -> Result[RT, CT, KT]: + """Take a collection of constraints, spit out the resolution result. + + This returns a representation of the final resolution state, with one + guarenteed attribute ``mapping`` that contains resolved candidates as + values. The keys are their respective identifiers. + + :param requirements: A collection of constraints. + :param kwargs: Additional keyword arguments that subclasses may accept. + + :raises: ``self.base_exception`` or its subclass. + """ + raise NotImplementedError diff --git a/src/pip/_vendor/resolvelib/resolvers/criterion.py b/src/pip/_vendor/resolvelib/resolvers/criterion.py new file mode 100644 index 00000000000..ee5019ccd03 --- /dev/null +++ b/src/pip/_vendor/resolvelib/resolvers/criterion.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import Collection, Generic, Iterable, Iterator + +from ..structs import CT, RT, RequirementInformation + + +class Criterion(Generic[RT, CT]): + """Representation of possible resolution results of a package. + + This holds three attributes: + + * `information` is a collection of `RequirementInformation` pairs. + Each pair is a requirement contributing to this criterion, and the + candidate that provides the requirement. + * `incompatibilities` is a collection of all known not-to-work candidates + to exclude from consideration. + * `candidates` is a collection containing all possible candidates deducted + from the union of contributing requirements and known incompatibilities. + It should never be empty, except when the criterion is an attribute of a + raised `RequirementsConflicted` (in which case it is always empty). + + .. note:: + This class is intended to be externally immutable. **Do not** mutate + any of its attribute containers. + """ + + def __init__( + self, + candidates: Iterable[CT], + information: Collection[RequirementInformation[RT, CT]], + incompatibilities: Collection[CT], + ) -> None: + self.candidates = candidates + self.information = information + self.incompatibilities = incompatibilities + + def __repr__(self) -> str: + requirements = ", ".join( + f"({req!r}, via={parent!r})" for req, parent in self.information + ) + return f"Criterion({requirements})" + + def iter_requirement(self) -> Iterator[RT]: + return (i.requirement for i in self.information) + + def iter_parent(self) -> Iterator[CT | None]: + return (i.parent for i in self.information) diff --git a/src/pip/_vendor/resolvelib/resolvers/exceptions.py b/src/pip/_vendor/resolvelib/resolvers/exceptions.py new file mode 100644 index 00000000000..35e275576f7 --- /dev/null +++ b/src/pip/_vendor/resolvelib/resolvers/exceptions.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Collection, Generic + +from ..structs import CT, RT, RequirementInformation + +if TYPE_CHECKING: + from .criterion import Criterion + + +class ResolverException(Exception): + """A base class for all exceptions raised by this module. + + Exceptions derived by this class should all be handled in this module. Any + bubbling pass the resolver should be treated as a bug. + """ + + +class RequirementsConflicted(ResolverException, Generic[RT, CT]): + def __init__(self, criterion: Criterion[RT, CT]) -> None: + super().__init__(criterion) + self.criterion = criterion + + def __str__(self) -> str: + return "Requirements conflict: {}".format( + ", ".join(repr(r) for r in self.criterion.iter_requirement()), + ) + + +class InconsistentCandidate(ResolverException, Generic[RT, CT]): + def __init__(self, candidate: CT, criterion: Criterion[RT, CT]): + super().__init__(candidate, criterion) + self.candidate = candidate + self.criterion = criterion + + def __str__(self) -> str: + return "Provided candidate {!r} does not satisfy {}".format( + self.candidate, + ", ".join(repr(r) for r in self.criterion.iter_requirement()), + ) + + +class ResolutionError(ResolverException): + pass + + +class ResolutionImpossible(ResolutionError, Generic[RT, CT]): + def __init__(self, causes: Collection[RequirementInformation[RT, CT]]): + super().__init__(causes) + # causes is a list of RequirementInformation objects + self.causes = causes + + +class ResolutionTooDeep(ResolutionError): + def __init__(self, round_count: int) -> None: + super().__init__(round_count) + self.round_count = round_count diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers/resolution.py similarity index 67% rename from src/pip/_vendor/resolvelib/resolvers.py rename to src/pip/_vendor/resolvelib/resolvers/resolution.py index 2c3d0e306f9..da3c66e2ab7 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers/resolution.py @@ -1,127 +1,90 @@ +from __future__ import annotations + import collections import itertools import operator - -from .providers import AbstractResolver -from .structs import DirectedGraph, IteratorMapping, build_iter_view - -RequirementInformation = collections.namedtuple( - "RequirementInformation", ["requirement", "parent"] +from typing import TYPE_CHECKING, Collection, Generic, Iterable, Mapping + +from ..structs import ( + CT, + KT, + RT, + DirectedGraph, + IterableView, + IteratorMapping, + RequirementInformation, + State, + build_iter_view, +) +from .abstract import AbstractResolver, Result +from .criterion import Criterion +from .exceptions import ( + InconsistentCandidate, + RequirementsConflicted, + ResolutionImpossible, + ResolutionTooDeep, + ResolverException, ) +if TYPE_CHECKING: + from ..providers import AbstractProvider, Preference + from ..reporters import BaseReporter -class ResolverException(Exception): - """A base class for all exceptions raised by this module. - - Exceptions derived by this class should all be handled in this module. Any - bubbling pass the resolver should be treated as a bug. - """ - - -class RequirementsConflicted(ResolverException): - def __init__(self, criterion): - super(RequirementsConflicted, self).__init__(criterion) - self.criterion = criterion - - def __str__(self): - return "Requirements conflict: {}".format( - ", ".join(repr(r) for r in self.criterion.iter_requirement()), - ) - - -class InconsistentCandidate(ResolverException): - def __init__(self, candidate, criterion): - super(InconsistentCandidate, self).__init__(candidate, criterion) - self.candidate = candidate - self.criterion = criterion - - def __str__(self): - return "Provided candidate {!r} does not satisfy {}".format( - self.candidate, - ", ".join(repr(r) for r in self.criterion.iter_requirement()), - ) - - -class Criterion(object): - """Representation of possible resolution results of a package. - - This holds three attributes: - - * `information` is a collection of `RequirementInformation` pairs. - Each pair is a requirement contributing to this criterion, and the - candidate that provides the requirement. - * `incompatibilities` is a collection of all known not-to-work candidates - to exclude from consideration. - * `candidates` is a collection containing all possible candidates deducted - from the union of contributing requirements and known incompatibilities. - It should never be empty, except when the criterion is an attribute of a - raised `RequirementsConflicted` (in which case it is always empty). - - .. note:: - This class is intended to be externally immutable. **Do not** mutate - any of its attribute containers. - """ - - def __init__(self, candidates, information, incompatibilities): - self.candidates = candidates - self.information = information - self.incompatibilities = incompatibilities - - def __repr__(self): - requirements = ", ".join( - "({!r}, via={!r})".format(req, parent) - for req, parent in self.information - ) - return "Criterion({})".format(requirements) - - def iter_requirement(self): - return (i.requirement for i in self.information) - - def iter_parent(self): - return (i.parent for i in self.information) - - -class ResolutionError(ResolverException): - pass - - -class ResolutionImpossible(ResolutionError): - def __init__(self, causes): - super(ResolutionImpossible, self).__init__(causes) - # causes is a list of RequirementInformation objects - self.causes = causes +def _build_result(state: State[RT, CT, KT]) -> Result[RT, CT, KT]: + mapping = state.mapping + all_keys: dict[int, KT | None] = {id(v): k for k, v in mapping.items()} + all_keys[id(None)] = None -class ResolutionTooDeep(ResolutionError): - def __init__(self, round_count): - super(ResolutionTooDeep, self).__init__(round_count) - self.round_count = round_count + graph: DirectedGraph[KT | None] = DirectedGraph() + graph.add(None) # Sentinel as root dependencies' parent. + connected: set[KT | None] = {None} + for key, criterion in state.criteria.items(): + if not _has_route_to_root(state.criteria, key, all_keys, connected): + continue + if key not in graph: + graph.add(key) + for p in criterion.iter_parent(): + try: + pkey = all_keys[id(p)] + except KeyError: + continue + if pkey not in graph: + graph.add(pkey) + graph.connect(pkey, key) -# Resolution state in a round. -State = collections.namedtuple("State", "mapping criteria backtrack_causes") + return Result( + mapping={k: v for k, v in mapping.items() if k in connected}, + graph=graph, + criteria=state.criteria, + ) -class Resolution(object): +class Resolution(Generic[RT, CT, KT]): """Stateful resolution object. This is designed as a one-off object that holds information to kick start the resolution process, and holds the results afterwards. """ - def __init__(self, provider, reporter): + def __init__( + self, + provider: AbstractProvider[RT, CT, KT], + reporter: BaseReporter[RT, CT, KT], + ) -> None: self._p = provider self._r = reporter - self._states = [] + self._states: list[State[RT, CT, KT]] = [] @property - def state(self): + def state(self) -> State[RT, CT, KT]: try: return self._states[-1] - except IndexError: - raise AttributeError("state") + except IndexError as e: + raise AttributeError("state") from e - def _push_new_state(self): + def _push_new_state(self) -> None: """Push a new state into history. This new state will be used to hold resolution results of the next @@ -135,7 +98,12 @@ def _push_new_state(self): ) self._states.append(state) - def _add_to_criteria(self, criteria, requirement, parent): + def _add_to_criteria( + self, + criteria: dict[KT, Criterion[RT, CT]], + requirement: RT, + parent: CT | None, + ) -> None: self._r.adding_requirement(requirement=requirement, parent=parent) identifier = self._p.identify(requirement_or_candidate=requirement) @@ -174,7 +142,9 @@ def _add_to_criteria(self, criteria, requirement, parent): raise RequirementsConflicted(criterion) criteria[identifier] = criterion - def _remove_information_from_criteria(self, criteria, parents): + def _remove_information_from_criteria( + self, criteria: dict[KT, Criterion[RT, CT]], parents: Collection[KT] + ) -> None: """Remove information from parents of criteria. Concretely, removes all values from each criterion's ``information`` @@ -199,7 +169,7 @@ def _remove_information_from_criteria(self, criteria, parents): criterion.incompatibilities, ) - def _get_preference(self, name): + def _get_preference(self, name: KT) -> Preference: return self._p.get_preference( identifier=name, resolutions=self.state.mapping, @@ -214,7 +184,9 @@ def _get_preference(self, name): backtrack_causes=self.state.backtrack_causes, ) - def _is_current_pin_satisfying(self, name, criterion): + def _is_current_pin_satisfying( + self, name: KT, criterion: Criterion[RT, CT] + ) -> bool: try: current_pin = self.state.mapping[name] except KeyError: @@ -224,16 +196,16 @@ def _is_current_pin_satisfying(self, name, criterion): for r in criterion.iter_requirement() ) - def _get_updated_criteria(self, candidate): + def _get_updated_criteria(self, candidate: CT) -> dict[KT, Criterion[RT, CT]]: criteria = self.state.criteria.copy() for requirement in self._p.get_dependencies(candidate=candidate): self._add_to_criteria(criteria, requirement, parent=candidate) return criteria - def _attempt_to_pin_criterion(self, name): + def _attempt_to_pin_criterion(self, name: KT) -> list[Criterion[RT, CT]]: criterion = self.state.criteria[name] - causes = [] + causes: list[Criterion[RT, CT]] = [] for candidate in criterion.candidates: try: criteria = self._get_updated_criteria(candidate) @@ -267,7 +239,42 @@ def _attempt_to_pin_criterion(self, name): # end, signal for backtracking. return causes - def _backjump(self, causes): + def _patch_criteria( + self, incompatibilities_from_broken: list[tuple[KT, list[CT]]] + ) -> bool: + # Create a new state from the last known-to-work one, and apply + # the previously gathered incompatibility information. + for k, incompatibilities in incompatibilities_from_broken: + if not incompatibilities: + continue + try: + criterion = self.state.criteria[k] + except KeyError: + continue + matches = self._p.find_matches( + identifier=k, + requirements=IteratorMapping( + self.state.criteria, + operator.methodcaller("iter_requirement"), + ), + incompatibilities=IteratorMapping( + self.state.criteria, + operator.attrgetter("incompatibilities"), + {k: incompatibilities}, + ), + ) + candidates: IterableView[CT] = build_iter_view(matches) + if not candidates: + return False + incompatibilities.extend(criterion.incompatibilities) + self.state.criteria[k] = Criterion( + candidates=candidates, + information=list(criterion.information), + incompatibilities=incompatibilities, + ) + return True + + def _backjump(self, causes: list[RequirementInformation[RT, CT]]) -> bool: """Perform backjumping. When we enter here, the stack is like this:: @@ -298,7 +305,7 @@ def _backjump(self, causes): the new Z and go back to step 2. 5b. If the incompatibilities apply cleanly, end backtracking. """ - incompatible_reqs = itertools.chain( + incompatible_reqs: Iterable[CT | RT] = itertools.chain( (c.parent for c in causes if c.parent is not None), (c.requirement for c in causes), ) @@ -307,66 +314,44 @@ def _backjump(self, causes): # Remove the state that triggered backtracking. del self._states[-1] - # Ensure to backtrack to a state that caused the incompatibility - incompatible_state = False - while not incompatible_state: + # Optimistically backtrack to a state that caused the incompatibility + broken_state = self.state + while True: # 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) + raise ResolutionImpossible(causes) from None + + # Only backjump if the current broken state is + # an incompatible dependency + if name not in incompatible_deps: + break + + # If the current dependencies and the incompatible dependencies + # are overlapping then we have found a cause of the incompatibility current_dependencies = { - self._p.identify(d) - for d in self._p.get_dependencies(candidate) + self._p.identify(d) for d in self._p.get_dependencies(candidate) } - incompatible_state = not current_dependencies.isdisjoint( - incompatible_deps - ) + if not current_dependencies.isdisjoint(incompatible_deps): + break + + # Fallback: We should not backtrack to the point where + # broken_state.mapping is empty, so stop backtracking for + # a chance for the resolution to recover + if not broken_state.mapping: + break incompatibilities_from_broken = [ - (k, list(v.incompatibilities)) - for k, v in broken_state.criteria.items() + (k, list(v.incompatibilities)) for k, v in broken_state.criteria.items() ] # Also mark the newly known incompatibility. incompatibilities_from_broken.append((name, [candidate])) - # Create a new state from the last known-to-work one, and apply - # the previously gathered incompatibility information. - def _patch_criteria(): - for k, incompatibilities in incompatibilities_from_broken: - if not incompatibilities: - continue - try: - criterion = self.state.criteria[k] - except KeyError: - continue - matches = self._p.find_matches( - identifier=k, - requirements=IteratorMapping( - self.state.criteria, - operator.methodcaller("iter_requirement"), - ), - incompatibilities=IteratorMapping( - self.state.criteria, - operator.attrgetter("incompatibilities"), - {k: incompatibilities}, - ), - ) - candidates = build_iter_view(matches) - if not candidates: - return False - incompatibilities.extend(criterion.incompatibilities) - self.state.criteria[k] = Criterion( - candidates=candidates, - information=list(criterion.information), - incompatibilities=incompatibilities, - ) - return True - self._push_new_state() - success = _patch_criteria() + success = self._patch_criteria(incompatibilities_from_broken) # It works! Let's work on this new state. if success: @@ -378,7 +363,13 @@ def _patch_criteria(): # No way to backtrack anymore. return False - def resolve(self, requirements, max_rounds): + def _extract_causes( + self, criteron: list[Criterion[RT, CT]] + ) -> list[RequirementInformation[RT, CT]]: + """Extract causes from list of criterion and deduplicate""" + return list({id(i): i for c in criteron for i in c.information}.values()) + + def resolve(self, requirements: Iterable[RT], max_rounds: int) -> State[RT, CT, KT]: if self._states: raise RuntimeError("already resolved") @@ -396,7 +387,7 @@ def resolve(self, requirements, max_rounds): try: self._add_to_criteria(self.state.criteria, r, parent=None) except RequirementsConflicted as e: - raise ResolutionImpossible(e.criterion.information) + raise ResolutionImpossible(e.criterion.information) from e # The root state is saved as a sentinel so the first ever pin can have # something to backtrack to if it fails. The root state is basically @@ -418,16 +409,42 @@ def resolve(self, requirements, max_rounds): return self.state # keep track of satisfied names to calculate diff after pinning - satisfied_names = set(self.state.criteria.keys()) - set( - unsatisfied_names - ) + satisfied_names = set(self.state.criteria.keys()) - set(unsatisfied_names) + + if len(unsatisfied_names) > 1: + narrowed_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, + ) + ) + else: + narrowed_unstatisfied_names = unsatisfied_names + + # If there are no unsatisfied names use unsatisfied names + if not narrowed_unstatisfied_names: + raise RuntimeError("narrow_requirement_selection returned 0 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 there is only 1 unsatisfied name skip calling self._get_preference + if len(narrowed_unstatisfied_names) > 1: + # Choose the most preferred unpinned criterion to try. + name = min(narrowed_unstatisfied_names, key=self._get_preference) + else: + name = narrowed_unstatisfied_names[0] - if failure_causes: - causes = [i for c in failure_causes for i in c.information] + failure_criterion = self._attempt_to_pin_criterion(name) + + if failure_criterion: + causes = self._extract_causes(failure_criterion) # 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) @@ -457,64 +474,16 @@ def resolve(self, requirements, max_rounds): raise ResolutionTooDeep(max_rounds) -def _has_route_to_root(criteria, key, all_keys, connected): - if key in connected: - return True - if key not in criteria: - return False - for p in criteria[key].iter_parent(): - try: - pkey = all_keys[id(p)] - except KeyError: - continue - if pkey in connected: - connected.add(key) - return True - if _has_route_to_root(criteria, pkey, all_keys, connected): - connected.add(key) - return True - return False - - -Result = collections.namedtuple("Result", "mapping graph criteria") - - -def _build_result(state): - mapping = state.mapping - all_keys = {id(v): k for k, v in mapping.items()} - all_keys[id(None)] = None - - graph = DirectedGraph() - graph.add(None) # Sentinel as root dependencies' parent. - - connected = {None} - for key, criterion in state.criteria.items(): - if not _has_route_to_root(state.criteria, key, all_keys, connected): - continue - if key not in graph: - graph.add(key) - for p in criterion.iter_parent(): - try: - pkey = all_keys[id(p)] - except KeyError: - continue - if pkey not in graph: - graph.add(pkey) - graph.connect(pkey, key) - - return Result( - mapping={k: v for k, v in mapping.items() if k in connected}, - graph=graph, - criteria=state.criteria, - ) - - -class Resolver(AbstractResolver): +class Resolver(AbstractResolver[RT, CT, KT]): """The thing that performs the actual resolution work.""" base_exception = ResolverException - def resolve(self, requirements, max_rounds=100): + def resolve( # type: ignore[override] + self, + requirements: Iterable[RT], + max_rounds: int = 100, + ) -> Result[RT, CT, KT]: """Take a collection of constraints, spit out the resolution result. The return value is a representation to the final resolution result. It @@ -545,3 +514,28 @@ def resolve(self, requirements, max_rounds=100): resolution = Resolution(self.provider, self.reporter) state = resolution.resolve(requirements, max_rounds=max_rounds) return _build_result(state) + + +def _has_route_to_root( + criteria: Mapping[KT, Criterion[RT, CT]], + key: KT | None, + all_keys: dict[int, KT | None], + connected: set[KT | None], +) -> bool: + if key in connected: + return True + if key not in criteria: + return False + assert key is not None + for p in criteria[key].iter_parent(): + try: + pkey = all_keys[id(p)] + except KeyError: + continue + if pkey in connected: + connected.add(key) + return True + if _has_route_to_root(criteria, pkey, all_keys, connected): + connected.add(key) + return True + return False diff --git a/src/pip/_vendor/resolvelib/structs.py b/src/pip/_vendor/resolvelib/structs.py index 359a34f6018..18c74d41548 100644 --- a/src/pip/_vendor/resolvelib/structs.py +++ b/src/pip/_vendor/resolvelib/structs.py @@ -1,34 +1,73 @@ -import itertools - -from .compat import collections_abc - +from __future__ import annotations -class DirectedGraph(object): +import itertools +from collections import namedtuple +from typing import ( + TYPE_CHECKING, + Callable, + Generic, + Iterable, + Iterator, + Mapping, + NamedTuple, + Sequence, + TypeVar, + Union, +) + +KT = TypeVar("KT") # Identifier. +RT = TypeVar("RT") # Requirement. +CT = TypeVar("CT") # Candidate. + +Matches = Union[Iterable[CT], Callable[[], Iterable[CT]]] + +if TYPE_CHECKING: + from .resolvers.criterion import Criterion + + class RequirementInformation(NamedTuple, Generic[RT, CT]): + requirement: RT + parent: CT | None + + class State(NamedTuple, Generic[RT, CT, KT]): + """Resolution state in a round.""" + + mapping: dict[KT, CT] + criteria: dict[KT, Criterion[RT, CT]] + backtrack_causes: list[RequirementInformation[RT, CT]] + +else: + RequirementInformation = namedtuple( + "RequirementInformation", ["requirement", "parent"] + ) + State = namedtuple("State", ["mapping", "criteria", "backtrack_causes"]) + + +class DirectedGraph(Generic[KT]): """A graph structure with directed edges.""" - def __init__(self): - self._vertices = set() - self._forwards = {} # -> Set[] - self._backwards = {} # -> Set[] + def __init__(self) -> None: + self._vertices: set[KT] = set() + self._forwards: dict[KT, set[KT]] = {} # -> Set[] + self._backwards: dict[KT, set[KT]] = {} # -> Set[] - def __iter__(self): + def __iter__(self) -> Iterator[KT]: return iter(self._vertices) - def __len__(self): + def __len__(self) -> int: return len(self._vertices) - def __contains__(self, key): + def __contains__(self, key: KT) -> bool: return key in self._vertices - def copy(self): + def copy(self) -> DirectedGraph[KT]: """Return a shallow copy of this graph.""" - other = DirectedGraph() + other = type(self)() other._vertices = set(self._vertices) other._forwards = {k: set(v) for k, v in self._forwards.items()} other._backwards = {k: set(v) for k, v in self._backwards.items()} return other - def add(self, key): + def add(self, key: KT) -> None: """Add a new vertex to the graph.""" if key in self._vertices: raise ValueError("vertex exists") @@ -36,7 +75,7 @@ def add(self, key): self._forwards[key] = set() self._backwards[key] = set() - def remove(self, key): + def remove(self, key: KT) -> None: """Remove a vertex from the graph, disconnecting all edges from/to it.""" self._vertices.remove(key) for f in self._forwards.pop(key): @@ -44,10 +83,10 @@ def remove(self, key): for t in self._backwards.pop(key): self._forwards[t].remove(key) - def connected(self, f, t): + def connected(self, f: KT, t: KT) -> bool: return f in self._backwards[t] and t in self._forwards[f] - def connect(self, f, t): + def connect(self, f: KT, t: KT) -> None: """Connect two existing vertices. Nothing happens if the vertices are already connected. @@ -57,56 +96,59 @@ def connect(self, f, t): self._forwards[f].add(t) self._backwards[t].add(f) - def iter_edges(self): + def iter_edges(self) -> Iterator[tuple[KT, KT]]: for f, children in self._forwards.items(): for t in children: yield f, t - def iter_children(self, key): + def iter_children(self, key: KT) -> Iterator[KT]: return iter(self._forwards[key]) - def iter_parents(self, key): + def iter_parents(self, key: KT) -> Iterator[KT]: return iter(self._backwards[key]) -class IteratorMapping(collections_abc.Mapping): - def __init__(self, mapping, accessor, appends=None): +class IteratorMapping(Mapping[KT, Iterator[CT]], Generic[RT, CT, KT]): + def __init__( + self, + mapping: Mapping[KT, RT], + accessor: Callable[[RT], Iterable[CT]], + appends: Mapping[KT, Iterable[CT]] | None = None, + ) -> None: self._mapping = mapping self._accessor = accessor - self._appends = appends or {} + self._appends: Mapping[KT, Iterable[CT]] = appends or {} - def __repr__(self): + def __repr__(self) -> str: return "IteratorMapping({!r}, {!r}, {!r})".format( self._mapping, self._accessor, self._appends, ) - def __bool__(self): + def __bool__(self) -> bool: return bool(self._mapping or self._appends) - __nonzero__ = __bool__ # XXX: Python 2. - - def __contains__(self, key): + def __contains__(self, key: object) -> bool: return key in self._mapping or key in self._appends - def __getitem__(self, k): + def __getitem__(self, k: KT) -> Iterator[CT]: try: v = self._mapping[k] except KeyError: return iter(self._appends[k]) return itertools.chain(self._accessor(v), self._appends.get(k, ())) - def __iter__(self): + def __iter__(self) -> Iterator[KT]: more = (k for k in self._appends if k not in self._mapping) return itertools.chain(self._mapping, more) - def __len__(self): + def __len__(self) -> int: more = sum(1 for k in self._appends if k not in self._mapping) return len(self._mapping) + more -class _FactoryIterableView(object): +class _FactoryIterableView(Iterable[RT]): """Wrap an iterator factory returned by `find_matches()`. Calling `iter()` on this class would invoke the underlying iterator @@ -115,56 +157,53 @@ class _FactoryIterableView(object): built-in Python sequence types. """ - def __init__(self, factory): + def __init__(self, factory: Callable[[], Iterable[RT]]) -> None: self._factory = factory - self._iterable = None + self._iterable: Iterable[RT] | None = None - def __repr__(self): - return "{}({})".format(type(self).__name__, list(self)) + def __repr__(self) -> str: + return f"{type(self).__name__}({list(self)})" - def __bool__(self): + def __bool__(self) -> bool: try: next(iter(self)) except StopIteration: return False return True - __nonzero__ = __bool__ # XXX: Python 2. - - def __iter__(self): - iterable = ( - self._factory() if self._iterable is None else self._iterable - ) + def __iter__(self) -> Iterator[RT]: + iterable = self._factory() if self._iterable is None else self._iterable self._iterable, current = itertools.tee(iterable) return current -class _SequenceIterableView(object): +class _SequenceIterableView(Iterable[RT]): """Wrap an iterable returned by find_matches(). This is essentially just a proxy to the underlying sequence that provides the same interface as `_FactoryIterableView`. """ - def __init__(self, sequence): + def __init__(self, sequence: Sequence[RT]): self._sequence = sequence - def __repr__(self): - return "{}({})".format(type(self).__name__, self._sequence) + def __repr__(self) -> str: + return f"{type(self).__name__}({self._sequence})" - def __bool__(self): + def __bool__(self) -> bool: return bool(self._sequence) - __nonzero__ = __bool__ # XXX: Python 2. - - def __iter__(self): + def __iter__(self) -> Iterator[RT]: return iter(self._sequence) -def build_iter_view(matches): +def build_iter_view(matches: Matches[CT]) -> Iterable[CT]: """Build an iterable view from the value returned by `find_matches()`.""" if callable(matches): return _FactoryIterableView(matches) - if not isinstance(matches, collections_abc.Sequence): + if not isinstance(matches, Sequence): matches = list(matches) return _SequenceIterableView(matches) + + +IterableView = Iterable diff --git a/src/pip/_vendor/resolvelib/structs.pyi b/src/pip/_vendor/resolvelib/structs.pyi deleted file mode 100644 index 0ac59f0f00a..00000000000 --- a/src/pip/_vendor/resolvelib/structs.pyi +++ /dev/null @@ -1,40 +0,0 @@ -from abc import ABCMeta -from typing import ( - Callable, - Container, - Generic, - Iterable, - Iterator, - Mapping, - Tuple, - TypeVar, - Union, -) - -KT = TypeVar("KT") # Identifier. -RT = TypeVar("RT") # Requirement. -CT = TypeVar("CT") # Candidate. -_T = TypeVar("_T") - -Matches = Union[Iterable[CT], Callable[[], Iterable[CT]]] - -class IteratorMapping(Mapping[KT, _T], metaclass=ABCMeta): - pass - -class IterableView(Container[CT], Iterable[CT], metaclass=ABCMeta): - pass - -class DirectedGraph(Generic[KT]): - def __iter__(self) -> Iterator[KT]: ... - def __len__(self) -> int: ... - def __contains__(self, key: KT) -> bool: ... - def copy(self) -> "DirectedGraph[KT]": ... - def add(self, key: KT) -> None: ... - def remove(self, key: KT) -> None: ... - def connected(self, f: KT, t: KT) -> bool: ... - def connect(self, f: KT, t: KT) -> None: ... - def iter_edges(self) -> Iterable[Tuple[KT, KT]]: ... - def iter_children(self, key: KT) -> Iterable[KT]: ... - def iter_parents(self, key: KT) -> Iterable[KT]: ... - -def build_iter_view(matches: Matches) -> IterableView[CT]: ... diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 2ba053a6e54..f7fe0b7b958 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ requests==2.32.3 rich==13.7.1 pygments==2.18.0 typing_extensions==4.12.2 -resolvelib==1.0.1 +resolvelib==1.1.0 setuptools==70.3.0 tomli==2.0.1 truststore==0.10.0 diff --git a/tests/unit/resolution_resolvelib/test_provider.py b/tests/unit/resolution_resolvelib/test_provider.py deleted file mode 100644 index 5f30e2bc1dd..00000000000 --- a/tests/unit/resolution_resolvelib/test_provider.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import TYPE_CHECKING, List, Optional - -from pip._vendor.resolvelib.resolvers import RequirementInformation - -from pip._internal.models.candidate import InstallationCandidate -from pip._internal.models.link import Link -from pip._internal.req.constructors import install_req_from_req_string -from pip._internal.resolution.resolvelib.factory import Factory -from pip._internal.resolution.resolvelib.provider import PipProvider -from pip._internal.resolution.resolvelib.requirements import SpecifierRequirement - -if TYPE_CHECKING: - from pip._internal.resolution.resolvelib.provider import PreferenceInformation - - -def build_requirement_information( - name: str, parent: Optional[InstallationCandidate] -) -> List["PreferenceInformation"]: - install_requirement = install_req_from_req_string(name) - # RequirementInformation is typed as a tuple, but it is a namedtupled. - # https://github.com/sarugaku/resolvelib/blob/7bc025aa2a4e979597c438ad7b17d2e8a08a364e/src/resolvelib/resolvers.pyi#L20-L22 - requirement_information: PreferenceInformation = RequirementInformation( - requirement=SpecifierRequirement(install_requirement), # type: ignore[call-arg] - parent=parent, - ) - return [requirement_information] - - -def test_provider_known_depths(factory: Factory) -> None: - # Root requirement is specified by the user - # therefore has an inferred depth of 1 - root_requirement_name = "my-package" - provider = PipProvider( - factory=factory, - constraints={}, - ignore_dependencies=False, - upgrade_strategy="to-satisfy-only", - user_requested={root_requirement_name: 0}, - ) - - root_requirement_information = build_requirement_information( - name=root_requirement_name, parent=None - ) - provider.get_preference( - identifier=root_requirement_name, - resolutions={}, - candidates={}, - information={root_requirement_name: root_requirement_information}, - backtrack_causes=[], - ) - assert provider._known_depths == {root_requirement_name: 1.0} - - # Transitive requirement is a dependency of root requirement - # theforefore has an inferred depth of 2 - root_package_candidate = InstallationCandidate( - root_requirement_name, - "1.0", - Link("https://{root_requirement_name}.com"), - ) - transitive_requirement_name = "my-transitive-package" - - transitive_package_information = build_requirement_information( - name=transitive_requirement_name, parent=root_package_candidate - ) - provider.get_preference( - identifier=transitive_requirement_name, - resolutions={}, - candidates={}, - information={ - root_requirement_name: root_requirement_information, - transitive_requirement_name: transitive_package_information, - }, - backtrack_causes=[], - ) - assert provider._known_depths == { - transitive_requirement_name: 2.0, - root_requirement_name: 1.0, - }