First Commit
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
||||
from functools import cache
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
from pip._vendor.resolvelib.providers import AbstractProvider
|
||||
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
from .base import Candidate, Constraint, Requirement
|
||||
from .candidates import REQUIRES_PYTHON_IDENTIFIER
|
||||
from .factory import Factory
|
||||
from .requirements import ExplicitRequirement
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pip._vendor.resolvelib.providers import Preference
|
||||
from pip._vendor.resolvelib.resolvers import RequirementInformation
|
||||
|
||||
PreferenceInformation = RequirementInformation[Requirement, Candidate]
|
||||
|
||||
_ProviderBase = AbstractProvider[Requirement, Candidate, str]
|
||||
else:
|
||||
_ProviderBase = AbstractProvider
|
||||
|
||||
_CONFLICT_PRIORITY_THRESHOLD = 5
|
||||
|
||||
# Notes on the relationship between the provider, the factory, and the
|
||||
# candidate and requirement classes.
|
||||
#
|
||||
# The provider is a direct implementation of the resolvelib class. Its role
|
||||
# is to deliver the API that resolvelib expects.
|
||||
#
|
||||
# Rather than work with completely abstract "requirement" and "candidate"
|
||||
# concepts as resolvelib does, pip has concrete classes implementing these two
|
||||
# ideas. The API of Requirement and Candidate objects are defined in the base
|
||||
# classes, but essentially map fairly directly to the equivalent provider
|
||||
# methods. In particular, `find_matches` and `is_satisfied_by` are
|
||||
# requirement methods, and `get_dependencies` is a candidate method.
|
||||
#
|
||||
# The factory is the interface to pip's internal mechanisms. It is stateless,
|
||||
# and is created by the resolver and held as a property of the provider. It is
|
||||
# responsible for creating Requirement and Candidate objects, and provides
|
||||
# services to those objects (access to pip's finder and preparer).
|
||||
|
||||
|
||||
D = TypeVar("D")
|
||||
V = TypeVar("V")
|
||||
|
||||
|
||||
def _get_with_identifier(
|
||||
mapping: Mapping[str, V],
|
||||
identifier: str,
|
||||
default: D,
|
||||
) -> D | V:
|
||||
"""Get item from a package name lookup mapping with a resolver identifier.
|
||||
|
||||
This extra logic is needed when the target mapping is keyed by package
|
||||
name, which cannot be directly looked up with an identifier (which may
|
||||
contain requested extras). Additional logic is added to also look up a value
|
||||
by "cleaning up" the extras from the identifier.
|
||||
"""
|
||||
if identifier in mapping:
|
||||
return mapping[identifier]
|
||||
# HACK: Theoretically we should check whether this identifier is a valid
|
||||
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
|
||||
# some regular expression. But since pip's resolver only spits out three
|
||||
# kinds of identifiers: normalized PEP 503 names, normalized names plus
|
||||
# extras, and Requires-Python, we can cheat a bit here.
|
||||
name, open_bracket, _ = identifier.partition("[")
|
||||
if open_bracket and name in mapping:
|
||||
return mapping[name]
|
||||
return default
|
||||
|
||||
|
||||
class PipProvider(_ProviderBase):
|
||||
"""Pip's provider implementation for resolvelib.
|
||||
|
||||
:params constraints: A mapping of constraints specified by the user. Keys
|
||||
are canonicalized project names.
|
||||
:params ignore_dependencies: Whether the user specified ``--no-deps``.
|
||||
:params upgrade_strategy: The user-specified upgrade strategy.
|
||||
:params user_requested: A set of canonicalized package names that the user
|
||||
supplied for pip to install/upgrade.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
factory: Factory,
|
||||
constraints: dict[str, Constraint],
|
||||
ignore_dependencies: bool,
|
||||
upgrade_strategy: str,
|
||||
user_requested: dict[str, int],
|
||||
) -> None:
|
||||
self._factory = factory
|
||||
self._constraints = constraints
|
||||
self._ignore_dependencies = ignore_dependencies
|
||||
self._upgrade_strategy = upgrade_strategy
|
||||
self._user_requested = user_requested
|
||||
self._conflict_counts: defaultdict[str, int] = defaultdict(int)
|
||||
self._conflict_promoted: set[str] = set()
|
||||
|
||||
@property
|
||||
def constraints(self) -> dict[str, Constraint]:
|
||||
"""Public view of user-specified constraints.
|
||||
|
||||
Exposes the provider's constraints mapping without encouraging
|
||||
external callers to reach into private attributes.
|
||||
"""
|
||||
return self._constraints
|
||||
|
||||
def identify(self, requirement_or_candidate: Requirement | Candidate) -> str:
|
||||
return requirement_or_candidate.name
|
||||
|
||||
def narrow_requirement_selection(
|
||||
self,
|
||||
identifiers: Iterable[str],
|
||||
resolutions: Mapping[str, Candidate],
|
||||
candidates: Mapping[str, Iterator[Candidate]],
|
||||
information: Mapping[str, Iterator[PreferenceInformation]],
|
||||
backtrack_causes: Sequence[PreferenceInformation],
|
||||
) -> Iterable[str]:
|
||||
"""Produce a subset of identifiers that should be considered before others.
|
||||
|
||||
Currently pip narrows the following selection:
|
||||
* Requires-Python, if present is always returned by itself
|
||||
* Backtrack causes are considered next because they can be identified
|
||||
in linear time here, whereas because get_preference() is called
|
||||
for each identifier, it would be quadratic to check for them there.
|
||||
Further, the current backtrack causes likely need to be resolved
|
||||
before other requirements as a resolution can't be found while
|
||||
there is a conflict.
|
||||
* Identifiers that repeatedly appear as not-yet-pinned in conflicts
|
||||
get promoted so they are resolved earlier. This lets their
|
||||
constraints take effect before other packages pick a version.
|
||||
"""
|
||||
backtrack_identifiers = set()
|
||||
for info in backtrack_causes:
|
||||
names = [info.requirement.name]
|
||||
if info.parent is not None:
|
||||
names.append(info.parent.name)
|
||||
for name in names:
|
||||
backtrack_identifiers.add(name)
|
||||
if name not in resolutions:
|
||||
self._conflict_counts[name] += 1
|
||||
if self._conflict_counts[name] >= _CONFLICT_PRIORITY_THRESHOLD:
|
||||
self._conflict_promoted.add(name)
|
||||
|
||||
current_backtrack_causes = []
|
||||
promoted = []
|
||||
for identifier in identifiers:
|
||||
if identifier == REQUIRES_PYTHON_IDENTIFIER:
|
||||
return [identifier]
|
||||
|
||||
if identifier in backtrack_identifiers:
|
||||
current_backtrack_causes.append(identifier)
|
||||
continue
|
||||
|
||||
if identifier in self._conflict_promoted:
|
||||
promoted.append(identifier)
|
||||
continue
|
||||
|
||||
if current_backtrack_causes:
|
||||
return current_backtrack_causes
|
||||
|
||||
if promoted:
|
||||
return promoted
|
||||
|
||||
return identifiers
|
||||
|
||||
def get_preference(
|
||||
self,
|
||||
identifier: str,
|
||||
resolutions: Mapping[str, Candidate],
|
||||
candidates: Mapping[str, Iterator[Candidate]],
|
||||
information: Mapping[str, Iterable[PreferenceInformation]],
|
||||
backtrack_causes: Sequence[PreferenceInformation],
|
||||
) -> Preference:
|
||||
"""Produce a sort key for given requirement based on preference.
|
||||
|
||||
The lower the return value is, the more preferred this group of
|
||||
arguments is.
|
||||
|
||||
Currently pip considers the following in order:
|
||||
|
||||
* Any requirement that is "direct", e.g., points to an explicit URL.
|
||||
* Any requirement that is "pinned", i.e., contains the operator ``===``
|
||||
or ``==`` without a wildcard.
|
||||
* Any requirement that imposes an upper version limit, i.e., contains the
|
||||
operator ``<``, ``<=``, ``~=``, or ``==`` with a wildcard. Because
|
||||
pip prioritizes the latest version, preferring explicit upper bounds
|
||||
can rule out infeasible candidates sooner. This does not imply that
|
||||
upper bounds are good practice; they can make dependency management
|
||||
and resolution harder.
|
||||
* Order user-specified requirements as they are specified, placing
|
||||
other requirements afterward.
|
||||
* Any "non-free" requirement, i.e., one that contains at least one
|
||||
operator, such as ``>=`` or ``!=``.
|
||||
* Alphabetical order for consistency (aids debuggability).
|
||||
"""
|
||||
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 not has_information:
|
||||
direct = False
|
||||
ireqs: tuple[InstallRequirement | None, ...] = ()
|
||||
else:
|
||||
# Go through the information and for each requirement,
|
||||
# check if it's explicit (e.g., a direct link) and get the
|
||||
# InstallRequirement (the second element) from get_candidate_lookup()
|
||||
directs, ireqs = zip(
|
||||
*(
|
||||
(isinstance(r, ExplicitRequirement), r.get_candidate_lookup()[1])
|
||||
for r, _ in information[identifier]
|
||||
)
|
||||
)
|
||||
direct = any(directs)
|
||||
|
||||
operators: list[tuple[str, str]] = [
|
||||
(specifier.operator, specifier.version)
|
||||
for specifier_set in (ireq.specifier for ireq in ireqs if ireq)
|
||||
for specifier in specifier_set
|
||||
]
|
||||
|
||||
pinned = any(((op[:2] == "==") and ("*" not in ver)) for op, ver in operators)
|
||||
upper_bounded = any(
|
||||
((op in ("<", "<=", "~=")) or (op == "==" and "*" in ver))
|
||||
for op, ver in operators
|
||||
)
|
||||
unfree = bool(operators)
|
||||
requested_order = self._user_requested.get(identifier, math.inf)
|
||||
|
||||
conflict_promoted = identifier in self._conflict_promoted
|
||||
|
||||
return (
|
||||
not conflict_promoted,
|
||||
not direct,
|
||||
not pinned,
|
||||
not upper_bounded,
|
||||
requested_order,
|
||||
not unfree,
|
||||
identifier,
|
||||
)
|
||||
|
||||
def find_matches(
|
||||
self,
|
||||
identifier: str,
|
||||
requirements: Mapping[str, Iterator[Requirement]],
|
||||
incompatibilities: Mapping[str, Iterator[Candidate]],
|
||||
) -> Iterable[Candidate]:
|
||||
def _eligible_for_upgrade(identifier: str) -> bool:
|
||||
"""Are upgrades allowed for this project?
|
||||
|
||||
This checks the upgrade strategy, and whether the project was one
|
||||
that the user specified in the command line, in order to decide
|
||||
whether we should upgrade if there's a newer version available.
|
||||
|
||||
(Note that we don't need access to the `--upgrade` flag, because
|
||||
an upgrade strategy of "to-satisfy-only" means that `--upgrade`
|
||||
was not specified).
|
||||
"""
|
||||
if self._upgrade_strategy == "eager":
|
||||
return True
|
||||
elif self._upgrade_strategy == "only-if-needed":
|
||||
user_order = _get_with_identifier(
|
||||
self._user_requested,
|
||||
identifier,
|
||||
default=None,
|
||||
)
|
||||
return user_order is not None
|
||||
return False
|
||||
|
||||
constraint = _get_with_identifier(
|
||||
self._constraints,
|
||||
identifier,
|
||||
default=Constraint.empty(),
|
||||
)
|
||||
return self._factory.find_candidates(
|
||||
identifier=identifier,
|
||||
requirements=requirements,
|
||||
constraint=constraint,
|
||||
prefers_installed=(not _eligible_for_upgrade(identifier)),
|
||||
incompatibilities=incompatibilities,
|
||||
is_satisfied_by=self.is_satisfied_by,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@cache
|
||||
def is_satisfied_by(requirement: Requirement, candidate: Candidate) -> bool:
|
||||
return requirement.is_satisfied_by(candidate)
|
||||
|
||||
def get_dependencies(self, candidate: Candidate) -> Iterable[Requirement]:
|
||||
with_requires = not self._ignore_dependencies
|
||||
# iter_dependencies() can perform nontrivial work so delay until needed.
|
||||
return (r for r in candidate.iter_dependencies(with_requires) if r is not None)
|
||||
Reference in New Issue
Block a user