diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst new file mode 100644 index 00000000000..c8b5c928d19 --- /dev/null +++ b/news/resolvelib.vendor.rst @@ -0,0 +1 @@ +Upgrade resolvelib to 0.9.0 diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 6300dfc57f0..b08cce7f333 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -104,7 +104,7 @@ def __init__( def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str: return requirement_or_candidate.name - def get_preference( # type: ignore + def get_preference( self, identifier: str, resolutions: Mapping[str, Candidate], @@ -124,14 +124,29 @@ def get_preference( # type: ignore * If equal, prefer if any requirement is "pinned", i.e. contains operator ``===`` or ``==``. * If equal, calculate an approximate "depth" and resolve requirements - closer to the user-specified requirements first. + closer to the user-specified requirements first. If the depth cannot + by determined (eg: due to no matching parents), it is considered + infinite. * Order user-specified requirements by the order they are specified. * If equal, prefers "non-free" requirements, i.e. contains at least one operator, such as ``>=`` or ``<``. * If equal, order alphabetically for consistency (helps debuggability). """ - lookups = (r.get_candidate_lookup() for r, _ in information[identifier]) - candidate, ireqs = zip(*lookups) + try: + next(iter(information[identifier])) + except StopIteration: + # There is no information for this identifier, so there's no known + # candidates. + has_information = False + else: + has_information = True + + if has_information: + lookups = (r.get_candidate_lookup() for r, _ in information[identifier]) + candidate, ireqs = zip(*lookups) + else: + candidate, ireqs = None, () + operators = [ specifier.operator for specifier_set in (ireq.specifier for ireq in ireqs if ireq) @@ -146,11 +161,14 @@ def get_preference( # type: ignore requested_order: Union[int, float] = self._user_requested[identifier] except KeyError: requested_order = math.inf - 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 + 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 diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 6ced5329b81..a95a8e4cf24 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -11,9 +11,9 @@ class PipReporter(BaseReporter): def __init__(self) -> None: - self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int) + self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int) - self._messages_at_backtrack = { + self._messages_at_reject_count = { 1: ( "pip is looking at multiple versions of {package_name} to " "determine which version is compatible with other " @@ -32,14 +32,14 @@ def __init__(self) -> None: ), } - def backtracking(self, candidate: Candidate) -> None: - self.backtracks_by_package[candidate.name] += 1 + def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None: + self.reject_count_by_package[candidate.name] += 1 - count = self.backtracks_by_package[candidate.name] - if count not in self._messages_at_backtrack: + count = self.reject_count_by_package[candidate.name] + if count not in self._messages_at_reject_count: return - message = self._messages_at_backtrack[count] + message = self._messages_at_reject_count[count] logger.info("INFO: %s", message.format(package_name=candidate.name)) @@ -61,8 +61,8 @@ def ending(self, state: Any) -> None: def adding_requirement(self, requirement: Requirement, parent: Candidate) -> None: logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent) - def backtracking(self, candidate: Candidate) -> None: - logger.info("Reporter.backtracking(%r)", candidate) + def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None: + logger.info("Reporter.rejecting_candidate(%r, %r)", criterion, candidate) def pinning(self, candidate: Candidate) -> None: logger.info("Reporter.pinning(%r)", candidate) diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index ce05fd30274..fa6995e32aa 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.8.1" +__version__ = "0.9.0" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/compat/collections_abc.pyi b/src/pip/_vendor/resolvelib/compat/collections_abc.pyi new file mode 100644 index 00000000000..2a088b19a93 --- /dev/null +++ b/src/pip/_vendor/resolvelib/compat/collections_abc.pyi @@ -0,0 +1 @@ +from collections.abc import Mapping, Sequence diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py index 7d0a9c22a46..e99d87ee75f 100644 --- a/src/pip/_vendor/resolvelib/providers.py +++ b/src/pip/_vendor/resolvelib/providers.py @@ -1,5 +1,5 @@ class AbstractProvider(object): - """Delegate class to provide requirement interface for the resolver.""" + """Delegate class to provide the required interface for the resolver.""" def identify(self, requirement_or_candidate): """Given a requirement, return an identifier for it. @@ -24,9 +24,9 @@ def get_preference( this group of arguments is. :param identifier: An identifier as returned by ``identify()``. This - identifies the dependency matches of which should be returned. + identifies the dependency matches which should be returned. :param resolutions: Mapping of candidates currently pinned by the - resolver. Each key is an identifier, and the value a candidate. + 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. @@ -39,10 +39,10 @@ def get_preference( * ``requirement`` specifies a requirement contributing to the current list of candidates. - * ``parent`` specifies the candidate that provides (dependend on) the + * ``parent`` specifies the candidate that provides (depended on) the requirement, or ``None`` to indicate a root requirement. - The preference could depend on a various of issues, including (not + The preference could depend on various issues, including (not necessarily in this order): * Is this package pinned in the current resolution result? @@ -61,7 +61,7 @@ def get_preference( raise NotImplementedError def find_matches(self, identifier, requirements, incompatibilities): - """Find all possible candidates that satisfy given constraints. + """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. @@ -92,7 +92,7 @@ def find_matches(self, identifier, requirements, incompatibilities): def is_satisfied_by(self, requirement, candidate): """Whether the given requirement can be satisfied by a candidate. - The candidate is guarenteed to have been generated from the + The candidate is guaranteed to have been generated from the requirement. A boolean should be returned to indicate whether ``candidate`` is a diff --git a/src/pip/_vendor/resolvelib/providers.pyi b/src/pip/_vendor/resolvelib/providers.pyi index 47d6f8abad7..ec054194ee3 100644 --- a/src/pip/_vendor/resolvelib/providers.pyi +++ b/src/pip/_vendor/resolvelib/providers.pyi @@ -1,12 +1,11 @@ from typing import ( Any, - Collection, Generic, Iterable, Iterator, Mapping, - Optional, Protocol, + Sequence, Union, ) @@ -25,6 +24,7 @@ class AbstractProvider(Generic[RT, CT, 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, diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py index 6695480fff4..688b5e10d86 100644 --- a/src/pip/_vendor/resolvelib/reporters.py +++ b/src/pip/_vendor/resolvelib/reporters.py @@ -36,7 +36,7 @@ def resolving_conflicts(self, causes): :param causes: The information on the collision that caused the backtracking. """ - def backtracking(self, candidate): + def rejecting_candidate(self, criterion, candidate): """Called when rejecting a candidate during backtracking.""" def pinning(self, candidate): diff --git a/src/pip/_vendor/resolvelib/reporters.pyi b/src/pip/_vendor/resolvelib/reporters.pyi index 03d4f09a390..b2ad286ba06 100644 --- a/src/pip/_vendor/resolvelib/reporters.pyi +++ b/src/pip/_vendor/resolvelib/reporters.pyi @@ -6,6 +6,6 @@ class BaseReporter: 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 backtracking(self, candidate: 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.py b/src/pip/_vendor/resolvelib/resolvers.py index 787681b03e9..49e30c7f5c4 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -173,6 +173,31 @@ def _add_to_criteria(self, criteria, requirement, parent): raise RequirementsConflicted(criterion) criteria[identifier] = criterion + def _remove_information_from_criteria(self, criteria, parents): + """Remove information from parents of criteria. + + Concretely, removes all values from each criterion's ``information`` + field that have one of ``parents`` as provider of the requirement. + + :param criteria: The criteria to update. + :param parents: Identifiers for which to remove information from all criteria. + """ + if not parents: + return + for key, criterion in criteria.items(): + criteria[key] = Criterion( + criterion.candidates, + [ + information + for information in criterion.information + if ( + information[1] is None + or self._p.identify(information[1]) not in parents + ) + ], + criterion.incompatibilities, + ) + def _get_preference(self, name): return self._p.get_preference( identifier=name, @@ -212,6 +237,7 @@ def _attempt_to_pin_criterion(self, name): try: criteria = self._get_updated_criteria(candidate) except RequirementsConflicted as e: + self._r.rejecting_candidate(e.criterion, candidate) causes.append(e.criterion) continue @@ -281,8 +307,6 @@ def _backtrack(self): # Also mark the newly known incompatibility. incompatibilities_from_broken.append((name, [candidate])) - self._r.backtracking(candidate=candidate) - # Create a new state from the last known-to-work one, and apply # the previously gathered incompatibility information. def _patch_criteria(): @@ -368,6 +392,11 @@ def resolve(self, requirements, max_rounds): self._r.ending(state=self.state) return self.state + # keep track of satisfied names to calculate diff after pinning + satisfied_names = set(self.state.criteria.keys()) - set( + unsatisfied_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) @@ -384,6 +413,17 @@ def resolve(self, requirements, max_rounds): if not success: raise ResolutionImpossible(self.state.backtrack_causes) else: + # discard as information sources any invalidated names + # (unsatisfied names that were previously satisfied) + newly_unsatisfied_names = { + key + for key, criterion in self.state.criteria.items() + if key in satisfied_names + and not self._is_current_pin_satisfying(key, criterion) + } + self._remove_information_from_criteria( + self.state.criteria, newly_unsatisfied_names + ) # Pinning was successful. Push a new state to do another pin. self._push_new_state() diff --git a/src/pip/_vendor/resolvelib/resolvers.pyi b/src/pip/_vendor/resolvelib/resolvers.pyi index 0eb5b2162c1..528a1a259af 100644 --- a/src/pip/_vendor/resolvelib/resolvers.pyi +++ b/src/pip/_vendor/resolvelib/resolvers.pyi @@ -55,6 +55,18 @@ class ResolutionImpossible(ResolutionError, Generic[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]] diff --git a/src/pip/_vendor/resolvelib/structs.py b/src/pip/_vendor/resolvelib/structs.py index 93d1568bd4d..359a34f6018 100644 --- a/src/pip/_vendor/resolvelib/structs.py +++ b/src/pip/_vendor/resolvelib/structs.py @@ -117,13 +117,14 @@ class _FactoryIterableView(object): def __init__(self, factory): self._factory = factory + self._iterable = None def __repr__(self): - return "{}({})".format(type(self).__name__, list(self._factory())) + return "{}({})".format(type(self).__name__, list(self)) def __bool__(self): try: - next(self._factory()) + next(iter(self)) except StopIteration: return False return True @@ -131,7 +132,11 @@ def __bool__(self): __nonzero__ = __bool__ # XXX: Python 2. def __iter__(self): - return self._factory() + iterable = ( + self._factory() if self._iterable is None else self._iterable + ) + self._iterable, current = itertools.tee(iterable) + return current class _SequenceIterableView(object): diff --git a/src/pip/_vendor/resolvelib/structs.pyi b/src/pip/_vendor/resolvelib/structs.pyi index fae2a2fcefc..0ac59f0f00a 100644 --- a/src/pip/_vendor/resolvelib/structs.pyi +++ b/src/pip/_vendor/resolvelib/structs.pyi @@ -16,7 +16,7 @@ RT = TypeVar("RT") # Requirement. CT = TypeVar("CT") # Candidate. _T = TypeVar("_T") -Matches = Union[Iterable[CT], Callable[[], Iterator[CT]]] +Matches = Union[Iterable[CT], Callable[[], Iterable[CT]]] class IteratorMapping(Mapping[KT, _T], metaclass=ABCMeta): pass diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 67452d89fcf..703daf196a6 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -15,7 +15,7 @@ requests==2.28.2 rich==12.6.0 pygments==2.13.0 typing_extensions==4.4.0 -resolvelib==0.8.1 +resolvelib==0.9.0 setuptools==44.0.0 six==1.16.0 tenacity==8.1.0