Загрузить файлы в «venv/Lib/site-packages/pip/_internal/resolution/resolvelib»
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
@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.
|
||||
"""
|
||||
backtrack_identifiers = set()
|
||||
for info in backtrack_causes:
|
||||
backtrack_identifiers.add(info.requirement.name)
|
||||
if info.parent is not None:
|
||||
backtrack_identifiers.add(info.parent.name)
|
||||
|
||||
current_backtrack_causes = []
|
||||
for identifier in identifiers:
|
||||
# Requires-Python has only one candidate and the check is basically
|
||||
# free, so we always do it first to avoid needless work if it fails.
|
||||
# This skips calling get_preference() for all other identifiers.
|
||||
if identifier == REQUIRES_PYTHON_IDENTIFIER:
|
||||
return [identifier]
|
||||
|
||||
# Check if this identifier is a backtrack cause
|
||||
if identifier in backtrack_identifiers:
|
||||
current_backtrack_causes.append(identifier)
|
||||
continue
|
||||
|
||||
if current_backtrack_causes:
|
||||
return current_backtrack_causes
|
||||
|
||||
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)
|
||||
|
||||
return (
|
||||
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)
|
||||
@@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from pip._vendor.resolvelib.reporters import BaseReporter
|
||||
|
||||
from .base import Candidate, Constraint, Requirement
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class PipReporter(BaseReporter[Requirement, Candidate, str]):
|
||||
def __init__(self, constraints: Mapping[str, Constraint] | None = None) -> None:
|
||||
self.reject_count_by_package: defaultdict[str, int] = defaultdict(int)
|
||||
self._constraints = constraints or {}
|
||||
|
||||
self._messages_at_reject_count = {
|
||||
1: (
|
||||
"pip is looking at multiple versions of {package_name} to "
|
||||
"determine which version is compatible with other "
|
||||
"requirements. This could take a while."
|
||||
),
|
||||
8: (
|
||||
"pip is still looking at multiple versions of {package_name} to "
|
||||
"determine which version is compatible with other "
|
||||
"requirements. This could take a while."
|
||||
),
|
||||
13: (
|
||||
"This is taking longer than usual. You might need to provide "
|
||||
"the dependency resolver with stricter constraints to reduce "
|
||||
"runtime. See https://pip.pypa.io/warnings/backtracking for "
|
||||
"guidance. If you want to abort this run, press Ctrl + C."
|
||||
),
|
||||
}
|
||||
|
||||
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
|
||||
"""Report a candidate being rejected.
|
||||
|
||||
Logs both the rejection count message (if applicable) and details about
|
||||
the requirements and constraints that caused the rejection.
|
||||
"""
|
||||
self.reject_count_by_package[candidate.name] += 1
|
||||
|
||||
count = self.reject_count_by_package[candidate.name]
|
||||
if count in self._messages_at_reject_count:
|
||||
message = self._messages_at_reject_count[count]
|
||||
logger.info("INFO: %s", message.format(package_name=candidate.name))
|
||||
|
||||
msg = "Will try a different candidate, due to conflict:"
|
||||
for req_info in criterion.information:
|
||||
req, parent = req_info.requirement, req_info.parent
|
||||
msg += "\n "
|
||||
if parent:
|
||||
msg += f"{parent.name} {parent.version} depends on "
|
||||
else:
|
||||
msg += "The user requested "
|
||||
msg += req.format_for_error()
|
||||
|
||||
# Add any relevant constraints
|
||||
if self._constraints:
|
||||
name = candidate.name
|
||||
constraint = self._constraints.get(name)
|
||||
if constraint and constraint.specifier:
|
||||
constraint_text = f"{name}{constraint.specifier}"
|
||||
msg += f"\n The user requested (constraint) {constraint_text}"
|
||||
|
||||
logger.debug(msg)
|
||||
|
||||
|
||||
class PipDebuggingReporter(BaseReporter[Requirement, Candidate, str]):
|
||||
"""A reporter that does an info log for every event it sees."""
|
||||
|
||||
def starting(self) -> None:
|
||||
logger.info("Reporter.starting()")
|
||||
|
||||
def starting_round(self, index: int) -> None:
|
||||
logger.info("Reporter.starting_round(%r)", index)
|
||||
|
||||
def ending_round(self, index: int, state: Any) -> None:
|
||||
logger.info("Reporter.ending_round(%r, state)", index)
|
||||
logger.debug("Reporter.ending_round(%r, %r)", index, state)
|
||||
|
||||
def ending(self, state: Any) -> None:
|
||||
logger.info("Reporter.ending(%r)", state)
|
||||
|
||||
def adding_requirement(
|
||||
self, requirement: Requirement, parent: Candidate | None
|
||||
) -> None:
|
||||
logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent)
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,251 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pip._vendor.packaging.specifiers import SpecifierSet
|
||||
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
||||
|
||||
from pip._internal.req.constructors import install_req_drop_extras
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
from .base import Candidate, CandidateLookup, Requirement, format_name
|
||||
|
||||
|
||||
class ExplicitRequirement(Requirement):
|
||||
def __init__(self, candidate: Candidate) -> None:
|
||||
self.candidate = candidate
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.candidate)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.candidate!r})"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.candidate)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, ExplicitRequirement):
|
||||
return False
|
||||
return self.candidate == other.candidate
|
||||
|
||||
@property
|
||||
def project_name(self) -> NormalizedName:
|
||||
# No need to canonicalize - the candidate did this
|
||||
return self.candidate.project_name
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
# No need to canonicalize - the candidate did this
|
||||
return self.candidate.name
|
||||
|
||||
def format_for_error(self) -> str:
|
||||
return self.candidate.format_for_error()
|
||||
|
||||
def get_candidate_lookup(self) -> CandidateLookup:
|
||||
return self.candidate, None
|
||||
|
||||
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
||||
return candidate == self.candidate
|
||||
|
||||
|
||||
class SpecifierRequirement(Requirement):
|
||||
def __init__(self, ireq: InstallRequirement) -> None:
|
||||
assert ireq.link is None, "This is a link, not a specifier"
|
||||
self._ireq = ireq
|
||||
self._equal_cache: str | None = None
|
||||
self._hash: int | None = None
|
||||
self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
|
||||
|
||||
@property
|
||||
def _equal(self) -> str:
|
||||
if self._equal_cache is not None:
|
||||
return self._equal_cache
|
||||
|
||||
self._equal_cache = str(self._ireq)
|
||||
return self._equal_cache
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self._ireq.req)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({str(self._ireq.req)!r})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, SpecifierRequirement):
|
||||
return NotImplemented
|
||||
return self._equal == other._equal
|
||||
|
||||
def __hash__(self) -> int:
|
||||
if self._hash is not None:
|
||||
return self._hash
|
||||
|
||||
self._hash = hash(self._equal)
|
||||
return self._hash
|
||||
|
||||
@property
|
||||
def project_name(self) -> NormalizedName:
|
||||
assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
|
||||
return canonicalize_name(self._ireq.req.name)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return format_name(self.project_name, self._extras)
|
||||
|
||||
def format_for_error(self) -> str:
|
||||
# Convert comma-separated specifiers into "A, B, ..., F and G"
|
||||
# This makes the specifier a bit more "human readable", without
|
||||
# risking a change in meaning. (Hopefully! Not all edge cases have
|
||||
# been checked)
|
||||
parts = [s.strip() for s in str(self).split(",")]
|
||||
if len(parts) == 0:
|
||||
return ""
|
||||
elif len(parts) == 1:
|
||||
return parts[0]
|
||||
|
||||
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
||||
|
||||
def get_candidate_lookup(self) -> CandidateLookup:
|
||||
return None, self._ireq
|
||||
|
||||
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
||||
assert candidate.name == self.name, (
|
||||
f"Internal issue: Candidate is not for this requirement "
|
||||
f"{candidate.name} vs {self.name}"
|
||||
)
|
||||
# We can safely always allow prereleases here since PackageFinder
|
||||
# already implements the prerelease logic, and would have filtered out
|
||||
# prerelease candidates if the user does not expect them.
|
||||
assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
|
||||
spec = self._ireq.req.specifier
|
||||
return spec.contains(candidate.version, prereleases=True)
|
||||
|
||||
|
||||
class SpecifierWithoutExtrasRequirement(SpecifierRequirement):
|
||||
"""
|
||||
Requirement backed by an install requirement on a base package.
|
||||
Trims extras from its install requirement if there are any.
|
||||
"""
|
||||
|
||||
def __init__(self, ireq: InstallRequirement) -> None:
|
||||
assert ireq.link is None, "This is a link, not a specifier"
|
||||
self._ireq = install_req_drop_extras(ireq)
|
||||
self._equal_cache: str | None = None
|
||||
self._hash: int | None = None
|
||||
self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
|
||||
|
||||
@property
|
||||
def _equal(self) -> str:
|
||||
if self._equal_cache is not None:
|
||||
return self._equal_cache
|
||||
|
||||
self._equal_cache = str(self._ireq)
|
||||
return self._equal_cache
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, SpecifierWithoutExtrasRequirement):
|
||||
return NotImplemented
|
||||
return self._equal == other._equal
|
||||
|
||||
def __hash__(self) -> int:
|
||||
if self._hash is not None:
|
||||
return self._hash
|
||||
|
||||
self._hash = hash(self._equal)
|
||||
return self._hash
|
||||
|
||||
|
||||
class RequiresPythonRequirement(Requirement):
|
||||
"""A requirement representing Requires-Python metadata."""
|
||||
|
||||
def __init__(self, specifier: SpecifierSet, match: Candidate) -> None:
|
||||
self.specifier = specifier
|
||||
self._specifier_string = str(specifier) # for faster __eq__
|
||||
self._hash: int | None = None
|
||||
self._candidate = match
|
||||
|
||||
# Pre-compute candidate lookup to avoid repeated specifier checks
|
||||
if specifier.contains(match.version, prereleases=True):
|
||||
self._candidate_lookup: CandidateLookup = (match, None)
|
||||
else:
|
||||
self._candidate_lookup = (None, None)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Python {self.specifier}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({str(self.specifier)!r})"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
if self._hash is not None:
|
||||
return self._hash
|
||||
|
||||
self._hash = hash((self._specifier_string, self._candidate))
|
||||
return self._hash
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, RequiresPythonRequirement):
|
||||
return False
|
||||
return (
|
||||
self._specifier_string == other._specifier_string
|
||||
and self._candidate == other._candidate
|
||||
)
|
||||
|
||||
@property
|
||||
def project_name(self) -> NormalizedName:
|
||||
return self._candidate.project_name
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._candidate.name
|
||||
|
||||
def format_for_error(self) -> str:
|
||||
return str(self)
|
||||
|
||||
def get_candidate_lookup(self) -> CandidateLookup:
|
||||
return self._candidate_lookup
|
||||
|
||||
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
||||
assert candidate.name == self._candidate.name, "Not Python candidate"
|
||||
# We can safely always allow prereleases here since PackageFinder
|
||||
# already implements the prerelease logic, and would have filtered out
|
||||
# prerelease candidates if the user does not expect them.
|
||||
return self.specifier.contains(candidate.version, prereleases=True)
|
||||
|
||||
|
||||
class UnsatisfiableRequirement(Requirement):
|
||||
"""A requirement that cannot be satisfied."""
|
||||
|
||||
def __init__(self, name: NormalizedName) -> None:
|
||||
self._name = name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self._name} (unavailable)"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({str(self._name)!r})"
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, UnsatisfiableRequirement):
|
||||
return NotImplemented
|
||||
return self._name == other._name
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._name)
|
||||
|
||||
@property
|
||||
def project_name(self) -> NormalizedName:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def format_for_error(self) -> str:
|
||||
return str(self)
|
||||
|
||||
def get_candidate_lookup(self) -> CandidateLookup:
|
||||
return None, None
|
||||
|
||||
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
||||
return False
|
||||
@@ -0,0 +1,332 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible, ResolutionTooDeep
|
||||
from pip._vendor.resolvelib import Resolver as RLResolver
|
||||
from pip._vendor.resolvelib.structs import DirectedGraph
|
||||
|
||||
from pip._internal.cache import WheelCache
|
||||
from pip._internal.exceptions import ResolutionTooDeepError
|
||||
from pip._internal.index.package_finder import PackageFinder
|
||||
from pip._internal.operations.prepare import RequirementPreparer
|
||||
from pip._internal.req.constructors import install_req_extend_extras
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.req.req_set import RequirementSet
|
||||
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
|
||||
from pip._internal.resolution.resolvelib.provider import PipProvider
|
||||
from pip._internal.resolution.resolvelib.reporter import (
|
||||
PipDebuggingReporter,
|
||||
PipReporter,
|
||||
)
|
||||
from pip._internal.utils.packaging import get_requirement
|
||||
|
||||
from .base import Candidate, Requirement
|
||||
from .factory import Factory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pip._vendor.resolvelib.resolvers import Result as RLResult
|
||||
|
||||
Result = RLResult[Requirement, Candidate, str]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Resolver(BaseResolver):
|
||||
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
preparer: RequirementPreparer,
|
||||
finder: PackageFinder,
|
||||
wheel_cache: WheelCache | None,
|
||||
make_install_req: InstallRequirementProvider,
|
||||
use_user_site: bool,
|
||||
ignore_dependencies: bool,
|
||||
ignore_installed: bool,
|
||||
ignore_requires_python: bool,
|
||||
force_reinstall: bool,
|
||||
upgrade_strategy: str,
|
||||
py_version_info: tuple[int, ...] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
assert upgrade_strategy in self._allowed_strategies
|
||||
|
||||
self.factory = Factory(
|
||||
finder=finder,
|
||||
preparer=preparer,
|
||||
make_install_req=make_install_req,
|
||||
wheel_cache=wheel_cache,
|
||||
use_user_site=use_user_site,
|
||||
force_reinstall=force_reinstall,
|
||||
ignore_installed=ignore_installed,
|
||||
ignore_requires_python=ignore_requires_python,
|
||||
py_version_info=py_version_info,
|
||||
)
|
||||
self.ignore_dependencies = ignore_dependencies
|
||||
self.upgrade_strategy = upgrade_strategy
|
||||
self._result: Result | None = None
|
||||
|
||||
def resolve(
|
||||
self, root_reqs: list[InstallRequirement], check_supported_wheels: bool
|
||||
) -> RequirementSet:
|
||||
collected = self.factory.collect_root_requirements(root_reqs)
|
||||
provider = PipProvider(
|
||||
factory=self.factory,
|
||||
constraints=collected.constraints,
|
||||
ignore_dependencies=self.ignore_dependencies,
|
||||
upgrade_strategy=self.upgrade_strategy,
|
||||
user_requested=collected.user_requested,
|
||||
)
|
||||
if "PIP_RESOLVER_DEBUG" in os.environ:
|
||||
reporter: BaseReporter[Requirement, Candidate, str] = PipDebuggingReporter()
|
||||
else:
|
||||
reporter = PipReporter(constraints=provider.constraints)
|
||||
|
||||
resolver: RLResolver[Requirement, Candidate, str] = RLResolver(
|
||||
provider,
|
||||
reporter,
|
||||
)
|
||||
|
||||
try:
|
||||
limit_how_complex_resolution_can_be = 200000
|
||||
result = self._result = resolver.resolve(
|
||||
collected.requirements, max_rounds=limit_how_complex_resolution_can_be
|
||||
)
|
||||
|
||||
except ResolutionImpossible as e:
|
||||
error = self.factory.get_installation_error(
|
||||
cast("ResolutionImpossible[Requirement, Candidate]", e),
|
||||
collected.constraints,
|
||||
)
|
||||
raise error from e
|
||||
except ResolutionTooDeep:
|
||||
raise ResolutionTooDeepError from None
|
||||
|
||||
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
|
||||
# process candidates with extras last to ensure their base equivalent is
|
||||
# already in the req_set if appropriate.
|
||||
# Python's sort is stable so using a binary key function keeps relative order
|
||||
# within both subsets.
|
||||
for candidate in sorted(
|
||||
result.mapping.values(), key=lambda c: c.name != c.project_name
|
||||
):
|
||||
ireq = candidate.get_install_requirement()
|
||||
if ireq is None:
|
||||
if candidate.name != candidate.project_name:
|
||||
# extend existing req's extras
|
||||
with contextlib.suppress(KeyError):
|
||||
req = req_set.get_requirement(candidate.project_name)
|
||||
req_set.add_named_requirement(
|
||||
install_req_extend_extras(
|
||||
req, get_requirement(candidate.name).extras
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Check if there is already an installation under the same name,
|
||||
# and set a flag for later stages to uninstall it, if needed.
|
||||
installed_dist = self.factory.get_dist_to_uninstall(candidate)
|
||||
if installed_dist is None:
|
||||
# There is no existing installation -- nothing to uninstall.
|
||||
ireq.should_reinstall = False
|
||||
elif self.factory.force_reinstall:
|
||||
# The --force-reinstall flag is set -- reinstall.
|
||||
ireq.should_reinstall = True
|
||||
elif installed_dist.version != candidate.version:
|
||||
# The installation is different in version -- reinstall.
|
||||
ireq.should_reinstall = True
|
||||
elif candidate.is_editable or installed_dist.editable:
|
||||
# The incoming distribution is editable, or different in
|
||||
# editable-ness to installation -- reinstall.
|
||||
ireq.should_reinstall = True
|
||||
elif candidate.source_link and candidate.source_link.is_file:
|
||||
# The incoming distribution is under file://
|
||||
if candidate.source_link.is_wheel:
|
||||
# is a local wheel -- do nothing.
|
||||
logger.info(
|
||||
"%s is already installed with the same version as the "
|
||||
"provided wheel. Use --force-reinstall to force an "
|
||||
"installation of the wheel.",
|
||||
ireq.name,
|
||||
)
|
||||
continue
|
||||
|
||||
# is a local sdist or path -- reinstall
|
||||
ireq.should_reinstall = True
|
||||
else:
|
||||
continue
|
||||
|
||||
link = candidate.source_link
|
||||
if link and link.is_yanked:
|
||||
# The reason can contain non-ASCII characters, Unicode
|
||||
# is required for Python 2.
|
||||
msg = (
|
||||
"The candidate selected for download or install is a "
|
||||
"yanked version: {name!r} candidate (version {version} "
|
||||
"at {link})\nReason for being yanked: {reason}"
|
||||
).format(
|
||||
name=candidate.name,
|
||||
version=candidate.version,
|
||||
link=link,
|
||||
reason=link.yanked_reason or "<none given>",
|
||||
)
|
||||
logger.warning(msg)
|
||||
|
||||
req_set.add_named_requirement(ireq)
|
||||
|
||||
return req_set
|
||||
|
||||
def get_installation_order(
|
||||
self, req_set: RequirementSet
|
||||
) -> list[InstallRequirement]:
|
||||
"""Get order for installation of requirements in RequirementSet.
|
||||
|
||||
The returned list contains a requirement before another that depends on
|
||||
it. This helps ensure that the environment is kept consistent as they
|
||||
get installed one-by-one.
|
||||
|
||||
The current implementation creates a topological ordering of the
|
||||
dependency graph, giving more weight to packages with less
|
||||
or no dependencies, while breaking any cycles in the graph at
|
||||
arbitrary points. We make no guarantees about where the cycle
|
||||
would be broken, other than it *would* be broken.
|
||||
"""
|
||||
assert self._result is not None, "must call resolve() first"
|
||||
|
||||
if not req_set.requirements:
|
||||
# Nothing is left to install, so we do not need an order.
|
||||
return []
|
||||
|
||||
graph = self._result.graph
|
||||
weights = get_topological_weights(graph, set(req_set.requirements.keys()))
|
||||
|
||||
sorted_items = sorted(
|
||||
req_set.requirements.items(),
|
||||
key=functools.partial(_req_set_item_sorter, weights=weights),
|
||||
reverse=True,
|
||||
)
|
||||
return [ireq for _, ireq in sorted_items]
|
||||
|
||||
|
||||
def get_topological_weights(
|
||||
graph: DirectedGraph[str | None], requirement_keys: set[str]
|
||||
) -> dict[str | None, int]:
|
||||
"""Assign weights to each node based on how "deep" they are.
|
||||
|
||||
This implementation may change at any point in the future without prior
|
||||
notice.
|
||||
|
||||
We first simplify the dependency graph by pruning any leaves and giving them
|
||||
the highest weight: a package without any dependencies should be installed
|
||||
first. This is done again and again in the same way, giving ever less weight
|
||||
to the newly found leaves. The loop stops when no leaves are left: all
|
||||
remaining packages have at least one dependency left in the graph.
|
||||
|
||||
Then we continue with the remaining graph, by taking the length for the
|
||||
longest path to any node from root, ignoring any paths that contain a single
|
||||
node twice (i.e. cycles). This is done through a depth-first search through
|
||||
the graph, while keeping track of the path to the node.
|
||||
|
||||
Cycles in the graph result would result in node being revisited while also
|
||||
being on its own path. In this case, take no action. This helps ensure we
|
||||
don't get stuck in a cycle.
|
||||
|
||||
When assigning weight, the longer path (i.e. larger length) is preferred.
|
||||
|
||||
We are only interested in the weights of packages that are in the
|
||||
requirement_keys.
|
||||
"""
|
||||
path: set[str | None] = set()
|
||||
weights: dict[str | None, list[int]] = {}
|
||||
|
||||
def visit(node: str | None) -> None:
|
||||
if node in path:
|
||||
# We hit a cycle, so we'll break it here.
|
||||
return
|
||||
|
||||
# The walk is exponential and for pathologically connected graphs (which
|
||||
# are the ones most likely to contain cycles in the first place) it can
|
||||
# take until the heat-death of the universe. To counter this we limit
|
||||
# the number of attempts to visit (i.e. traverse through) any given
|
||||
# node. We choose a value here which gives decent enough coverage for
|
||||
# fairly well behaved graphs, and still limits the walk complexity to be
|
||||
# linear in nature.
|
||||
cur_weights = weights.get(node, [])
|
||||
if len(cur_weights) >= 5:
|
||||
return
|
||||
|
||||
# Time to visit the children!
|
||||
path.add(node)
|
||||
for child in graph.iter_children(node):
|
||||
visit(child)
|
||||
path.remove(node)
|
||||
|
||||
if node not in requirement_keys:
|
||||
return
|
||||
|
||||
cur_weights.append(len(path))
|
||||
weights[node] = cur_weights
|
||||
|
||||
# Simplify the graph, pruning leaves that have no dependencies. This is
|
||||
# needed for large graphs (say over 200 packages) because the `visit`
|
||||
# function is slower for large/densely connected graphs, taking minutes.
|
||||
# See https://github.com/pypa/pip/issues/10557
|
||||
# We repeat the pruning step until we have no more leaves to remove.
|
||||
while True:
|
||||
leaves = set()
|
||||
for key in graph:
|
||||
if key is None:
|
||||
continue
|
||||
for _child in graph.iter_children(key):
|
||||
# This means we have at least one child
|
||||
break
|
||||
else:
|
||||
# No child.
|
||||
leaves.add(key)
|
||||
if not leaves:
|
||||
# We are done simplifying.
|
||||
break
|
||||
# Calculate the weight for the leaves.
|
||||
weight = len(graph) - 1
|
||||
for leaf in leaves:
|
||||
if leaf not in requirement_keys:
|
||||
continue
|
||||
weights[leaf] = [weight]
|
||||
# Remove the leaves from the graph, making it simpler.
|
||||
for leaf in leaves:
|
||||
graph.remove(leaf)
|
||||
|
||||
# Visit the remaining graph, this will only have nodes to handle if the
|
||||
# graph had a cycle in it, which the pruning step above could not handle.
|
||||
# `None` is guaranteed to be the root node by resolvelib.
|
||||
visit(None)
|
||||
|
||||
# Sanity check: all requirement keys should be in the weights,
|
||||
# and no other keys should be in the weights.
|
||||
difference = set(weights.keys()).difference(requirement_keys)
|
||||
assert not difference, difference
|
||||
|
||||
# Now give back all the weights, choosing the largest ones from what we
|
||||
# accumulated.
|
||||
return {node: max(wgts) for (node, wgts) in weights.items()}
|
||||
|
||||
|
||||
def _req_set_item_sorter(
|
||||
item: tuple[str, InstallRequirement],
|
||||
weights: dict[str | None, int],
|
||||
) -> tuple[int, str]:
|
||||
"""Key function used to sort install requirements for installation.
|
||||
|
||||
Based on the "weight" mapping calculated in ``get_installation_order()``.
|
||||
The canonical package name is returned as the second member as a tie-
|
||||
breaker to ensure the result is predictable, which is useful in tests.
|
||||
"""
|
||||
name = canonicalize_name(item[0])
|
||||
return weights[name], name
|
||||
Reference in New Issue
Block a user