Загрузить файлы в «venv/Lib/site-packages/pip/_internal/resolution/resolvelib»

This commit is contained in:
2026-07-02 19:09:02 +00:00
parent 786a7f0144
commit 0c6a8edccb
5 changed files with 1755 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Optional
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import Version
from pip._internal.models.link import Link, links_equivalent
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.hashes import Hashes
CandidateLookup = tuple[Optional["Candidate"], Optional[InstallRequirement]]
def format_name(project: NormalizedName, extras: frozenset[NormalizedName]) -> str:
if not extras:
return project
extras_expr = ",".join(sorted(extras))
return f"{project}[{extras_expr}]"
@dataclass(frozen=True)
class Constraint:
specifier: SpecifierSet
hashes: Hashes
links: frozenset[Link]
@classmethod
def empty(cls) -> Constraint:
return Constraint(SpecifierSet(), Hashes(), frozenset())
@classmethod
def from_ireq(cls, ireq: InstallRequirement) -> Constraint:
links = frozenset([ireq.link]) if ireq.link else frozenset()
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
def __bool__(self) -> bool:
return bool(self.specifier) or bool(self.hashes) or bool(self.links)
def __and__(self, other: InstallRequirement) -> Constraint:
if not isinstance(other, InstallRequirement):
return NotImplemented
specifier = self.specifier & other.specifier
hashes = self.hashes & other.hashes(trust_internet=False)
links = self.links
if other.link:
links = links.union([other.link])
return Constraint(specifier, hashes, links)
def is_satisfied_by(self, candidate: Candidate) -> bool:
# Reject if there are any mismatched URL constraints on this package.
if self.links and not all(_match_link(link, candidate) for link in self.links):
return False
# 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 Requirement:
@property
def project_name(self) -> NormalizedName:
"""The "project name" of a requirement.
This is different from ``name`` if this requirement contains extras,
in which case ``name`` would contain the ``[...]`` part, while this
refers to the name of the project.
"""
raise NotImplementedError("Subclass should override")
@property
def name(self) -> str:
"""The name identifying this requirement in the resolver.
This is different from ``project_name`` if this requirement contains
extras, where ``project_name`` would not contain the ``[...]`` part.
"""
raise NotImplementedError("Subclass should override")
def is_satisfied_by(self, candidate: Candidate) -> bool:
return False
def get_candidate_lookup(self) -> CandidateLookup:
raise NotImplementedError("Subclass should override")
def format_for_error(self) -> str:
raise NotImplementedError("Subclass should override")
def _match_link(link: Link, candidate: Candidate) -> bool:
if candidate.source_link:
return links_equivalent(link, candidate.source_link)
return False
class Candidate:
@property
def project_name(self) -> NormalizedName:
"""The "project name" of the candidate.
This is different from ``name`` if this candidate contains extras,
in which case ``name`` would contain the ``[...]`` part, while this
refers to the name of the project.
"""
raise NotImplementedError("Override in subclass")
@property
def name(self) -> str:
"""The name identifying this candidate in the resolver.
This is different from ``project_name`` if this candidate contains
extras, where ``project_name`` would not contain the ``[...]`` part.
"""
raise NotImplementedError("Override in subclass")
@property
def version(self) -> Version:
raise NotImplementedError("Override in subclass")
@property
def is_installed(self) -> bool:
raise NotImplementedError("Override in subclass")
@property
def is_editable(self) -> bool:
raise NotImplementedError("Override in subclass")
@property
def source_link(self) -> Link | None:
raise NotImplementedError("Override in subclass")
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
raise NotImplementedError("Override in subclass")
def get_install_requirement(self) -> InstallRequirement | None:
raise NotImplementedError("Override in subclass")
def format_for_error(self) -> str:
raise NotImplementedError("Subclass should override")

View File

@@ -0,0 +1,591 @@
from __future__ import annotations
import logging
import sys
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Union, cast
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version
from pip._internal.exceptions import (
FailedToPrepareCandidate,
HashError,
InstallationSubprocessError,
InvalidInstalledPackage,
MetadataInconsistent,
MetadataInvalid,
)
from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link, links_equivalent
from pip._internal.models.wheel import Wheel
from pip._internal.req.constructors import (
install_req_from_editable,
install_req_from_line,
)
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.direct_url_helpers import direct_url_from_link
from pip._internal.utils.misc import normalize_version_info
from .base import Candidate, Requirement, format_name
if TYPE_CHECKING:
from .factory import Factory
logger = logging.getLogger(__name__)
BaseCandidate = Union[
"AlreadyInstalledCandidate",
"EditableCandidate",
"LinkCandidate",
]
# Avoid conflicting with the PyPI package "Python".
REQUIRES_PYTHON_IDENTIFIER = cast(NormalizedName, "<Python from Requires-Python>")
def as_base_candidate(candidate: Candidate) -> BaseCandidate | None:
"""The runtime version of BaseCandidate."""
base_candidate_classes = (
AlreadyInstalledCandidate,
EditableCandidate,
LinkCandidate,
)
if isinstance(candidate, base_candidate_classes):
return candidate
return None
def make_install_req_from_link(
link: Link, template: InstallRequirement
) -> InstallRequirement:
assert not template.editable, "template is editable"
if template.req:
line = str(template.req)
else:
line = link.url
ireq = install_req_from_line(
line,
user_supplied=template.user_supplied,
comes_from=template.comes_from,
isolated=template.isolated,
constraint=template.constraint,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
ireq.original_link = template.original_link
ireq.link = link
ireq.extras = template.extras
return ireq
def make_install_req_from_editable(
link: Link, template: InstallRequirement
) -> InstallRequirement:
assert template.editable, "template not editable"
if template.name:
req_string = f"{template.name} @ {link.url}"
else:
req_string = link.url
ireq = install_req_from_editable(
req_string,
user_supplied=template.user_supplied,
comes_from=template.comes_from,
isolated=template.isolated,
constraint=template.constraint,
permit_editable_wheels=template.permit_editable_wheels,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
ireq.extras = template.extras
return ireq
def _make_install_req_from_dist(
dist: BaseDistribution, template: InstallRequirement
) -> InstallRequirement:
if template.req:
line = str(template.req)
elif template.link:
line = f"{dist.canonical_name} @ {template.link.url}"
else:
line = f"{dist.canonical_name}=={dist.version}"
ireq = install_req_from_line(
line,
user_supplied=template.user_supplied,
comes_from=template.comes_from,
isolated=template.isolated,
constraint=template.constraint,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
ireq.satisfied_by = dist
return ireq
class _InstallRequirementBackedCandidate(Candidate):
"""A candidate backed by an ``InstallRequirement``.
This represents a package request with the target not being already
in the environment, and needs to be fetched and installed. The backing
``InstallRequirement`` is responsible for most of the leg work; this
class exposes appropriate information to the resolver.
:param link: The link passed to the ``InstallRequirement``. The backing
``InstallRequirement`` will use this link to fetch the distribution.
:param source_link: The link this candidate "originates" from. This is
different from ``link`` when the link is found in the wheel cache.
``link`` would point to the wheel cache, while this points to the
found remote link (e.g. from pypi.org).
"""
dist: BaseDistribution
is_installed = False
def __init__(
self,
link: Link,
source_link: Link,
ireq: InstallRequirement,
factory: Factory,
name: NormalizedName | None = None,
version: Version | None = None,
) -> None:
self._link = link
self._source_link = source_link
self._factory = factory
self._ireq = ireq
self._name = name
self._version = version
self.dist = self._prepare()
self._hash: int | None = None
def __str__(self) -> str:
return f"{self.name} {self.version}"
def __repr__(self) -> str:
return f"{self.__class__.__name__}({str(self._link)!r})"
def __hash__(self) -> int:
if self._hash is not None:
return self._hash
self._hash = hash((self.__class__, self._link))
return self._hash
def __eq__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return links_equivalent(self._link, other._link)
return False
@property
def source_link(self) -> Link | None:
return self._source_link
@property
def project_name(self) -> NormalizedName:
"""The normalised name of the project the candidate refers to"""
if self._name is None:
self._name = self.dist.canonical_name
return self._name
@property
def name(self) -> str:
return self.project_name
@property
def version(self) -> Version:
if self._version is None:
self._version = self.dist.version
return self._version
def format_for_error(self) -> str:
return (
f"{self.name} {self.version} "
f"(from {self._link.file_path if self._link.is_file else self._link})"
)
def _prepare_distribution(self) -> BaseDistribution:
raise NotImplementedError("Override in subclass")
def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
"""Check for consistency of project name and version of dist."""
if self._name is not None and self._name != dist.canonical_name:
raise MetadataInconsistent(
self._ireq,
"name",
self._name,
dist.canonical_name,
)
if self._version is not None and self._version != dist.version:
raise MetadataInconsistent(
self._ireq,
"version",
str(self._version),
str(dist.version),
)
# check dependencies are valid
# TODO performance: this means we iterate the dependencies at least twice,
# we may want to cache parsed Requires-Dist
try:
list(dist.iter_dependencies(list(dist.iter_provided_extras())))
except InvalidRequirement as e:
raise MetadataInvalid(self._ireq, str(e))
def _prepare(self) -> BaseDistribution:
try:
dist = self._prepare_distribution()
except HashError as e:
# Provide HashError the underlying ireq that caused it. This
# provides context for the resulting error message to show the
# offending line to the user.
e.req = self._ireq
raise
except InstallationSubprocessError as exc:
if isinstance(self._ireq.comes_from, InstallRequirement):
request_chain = self._ireq.comes_from.from_path()
else:
request_chain = self._ireq.comes_from
if request_chain is None:
request_chain = "directly requested"
raise FailedToPrepareCandidate(
package_name=self._ireq.name or str(self._link),
requirement_chain=request_chain,
failed_step=exc.command_description,
)
self._check_metadata_consistency(dist)
return dist
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
# Emit the Requires-Python requirement first to fail fast on
# unsupported candidates and avoid pointless downloads/preparation.
yield self._factory.make_requires_python_requirement(self.dist.requires_python)
requires = self.dist.iter_dependencies() if with_requires else ()
for r in requires:
yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
def get_install_requirement(self) -> InstallRequirement | None:
return self._ireq
class LinkCandidate(_InstallRequirementBackedCandidate):
is_editable = False
def __init__(
self,
link: Link,
template: InstallRequirement,
factory: Factory,
name: NormalizedName | None = None,
version: Version | None = None,
) -> None:
source_link = link
cache_entry = factory.get_wheel_cache_entry(source_link, name)
if cache_entry is not None:
logger.debug("Using cached wheel link: %s", cache_entry.link)
link = cache_entry.link
ireq = make_install_req_from_link(link, template)
assert ireq.link == link
if ireq.link.is_wheel and not ireq.link.is_file:
wheel = Wheel(ireq.link.filename)
wheel_name = wheel.name
assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel"
# Version may not be present for PEP 508 direct URLs
if version is not None:
wheel_version = Version(wheel.version)
assert (
version == wheel_version
), f"{version!r} != {wheel_version!r} for wheel {name}"
if cache_entry is not None:
assert ireq.link.is_wheel
assert ireq.link.is_file
if cache_entry.persistent and template.link is template.original_link:
ireq.cached_wheel_source_link = source_link
if cache_entry.origin is not None:
ireq.download_info = cache_entry.origin
else:
# Legacy cache entry that does not have origin.json.
# download_info may miss the archive_info.hashes field.
ireq.download_info = direct_url_from_link(
source_link, link_is_in_wheel_cache=cache_entry.persistent
)
super().__init__(
link=link,
source_link=source_link,
ireq=ireq,
factory=factory,
name=name,
version=version,
)
def _prepare_distribution(self) -> BaseDistribution:
preparer = self._factory.preparer
return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
class EditableCandidate(_InstallRequirementBackedCandidate):
is_editable = True
def __init__(
self,
link: Link,
template: InstallRequirement,
factory: Factory,
name: NormalizedName | None = None,
version: Version | None = None,
) -> None:
super().__init__(
link=link,
source_link=link,
ireq=make_install_req_from_editable(link, template),
factory=factory,
name=name,
version=version,
)
def _prepare_distribution(self) -> BaseDistribution:
return self._factory.preparer.prepare_editable_requirement(self._ireq)
class AlreadyInstalledCandidate(Candidate):
is_installed = True
source_link = None
def __init__(
self,
dist: BaseDistribution,
template: InstallRequirement,
factory: Factory,
) -> None:
self.dist = dist
self._ireq = _make_install_req_from_dist(dist, template)
self._factory = factory
self._version = None
# This is just logging some messages, so we can do it eagerly.
# The returned dist would be exactly the same as self.dist because we
# set satisfied_by in _make_install_req_from_dist.
# TODO: Supply reason based on force_reinstall and upgrade_strategy.
skip_reason = "already satisfied"
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
def __str__(self) -> str:
return str(self.dist)
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.dist!r})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, AlreadyInstalledCandidate):
return NotImplemented
return self.name == other.name and self.version == other.version
def __hash__(self) -> int:
return hash((self.name, self.version))
@property
def project_name(self) -> NormalizedName:
return self.dist.canonical_name
@property
def name(self) -> str:
return self.project_name
@property
def version(self) -> Version:
if self._version is None:
self._version = self.dist.version
return self._version
@property
def is_editable(self) -> bool:
return self.dist.editable
def format_for_error(self) -> str:
return f"{self.name} {self.version} (Installed)"
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
if not with_requires:
return
try:
for r in self.dist.iter_dependencies():
yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
except InvalidRequirement as exc:
raise InvalidInstalledPackage(dist=self.dist, invalid_exc=exc) from None
def get_install_requirement(self) -> InstallRequirement | None:
return None
class ExtrasCandidate(Candidate):
"""A candidate that has 'extras', indicating additional dependencies.
Requirements can be for a project with dependencies, something like
foo[extra]. The extras don't affect the project/version being installed
directly, but indicate that we need additional dependencies. We model that
by having an artificial ExtrasCandidate that wraps the "base" candidate.
The ExtrasCandidate differs from the base in the following ways:
1. It has a unique name, of the form foo[extra]. This causes the resolver
to treat it as a separate node in the dependency graph.
2. When we're getting the candidate's dependencies,
a) We specify that we want the extra dependencies as well.
b) We add a dependency on the base candidate.
See below for why this is needed.
3. We return None for the underlying InstallRequirement, as the base
candidate will provide it, and we don't want to end up with duplicates.
The dependency on the base candidate is needed so that the resolver can't
decide that it should recommend foo[extra1] version 1.0 and foo[extra2]
version 2.0. Having those candidates depend on foo=1.0 and foo=2.0
respectively forces the resolver to recognise that this is a conflict.
"""
def __init__(
self,
base: BaseCandidate,
extras: frozenset[str],
*,
comes_from: InstallRequirement | None = None,
) -> None:
"""
:param comes_from: the InstallRequirement that led to this candidate if it
differs from the base's InstallRequirement. This will often be the
case in the sense that this candidate's requirement has the extras
while the base's does not. Unlike the InstallRequirement backed
candidates, this requirement is used solely for reporting purposes,
it does not do any leg work.
"""
self.base = base
self.extras = frozenset(canonicalize_name(e) for e in extras)
self._comes_from = comes_from if comes_from is not None else self.base._ireq
def __str__(self) -> str:
name, rest = str(self.base).split(" ", 1)
return "{}[{}] {}".format(name, ",".join(self.extras), rest)
def __repr__(self) -> str:
return f"{self.__class__.__name__}(base={self.base!r}, extras={self.extras!r})"
def __hash__(self) -> int:
return hash((self.base, self.extras))
def __eq__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.base == other.base and self.extras == other.extras
return False
@property
def project_name(self) -> NormalizedName:
return self.base.project_name
@property
def name(self) -> str:
"""The normalised name of the project the candidate refers to"""
return format_name(self.base.project_name, self.extras)
@property
def version(self) -> Version:
return self.base.version
def format_for_error(self) -> str:
return "{} [{}]".format(
self.base.format_for_error(), ", ".join(sorted(self.extras))
)
@property
def is_installed(self) -> bool:
return self.base.is_installed
@property
def is_editable(self) -> bool:
return self.base.is_editable
@property
def source_link(self) -> Link | None:
return self.base.source_link
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
factory = self.base._factory
# Add a dependency on the exact base
# (See note 2b in the class docstring)
yield factory.make_requirement_from_candidate(self.base)
if not with_requires:
return
# The user may have specified extras that the candidate doesn't
# support. We ignore any unsupported extras here.
valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
for extra in sorted(invalid_extras):
logger.warning(
"%s %s does not provide the extra '%s'",
self.base.name,
self.version,
extra,
)
for r in self.base.dist.iter_dependencies(valid_extras):
yield from factory.make_requirements_from_spec(
str(r),
self._comes_from,
valid_extras,
)
def get_install_requirement(self) -> InstallRequirement | None:
# We don't return anything here, because we always
# depend on the base candidate, and we'll get the
# install requirement from that.
return None
class RequiresPythonCandidate(Candidate):
is_installed = False
source_link = None
def __init__(self, py_version_info: tuple[int, ...] | None) -> None:
if py_version_info is not None:
version_info = normalize_version_info(py_version_info)
else:
version_info = sys.version_info[:3]
self._version = Version(".".join(str(c) for c in version_info))
# We don't need to implement __eq__() and __ne__() since there is always
# only one RequiresPythonCandidate in a resolution, i.e. the host Python.
# The built-in object.__eq__() and object.__ne__() do exactly what we want.
def __str__(self) -> str:
return f"Python {self._version}"
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self._version!r})"
@property
def project_name(self) -> NormalizedName:
return REQUIRES_PYTHON_IDENTIFIER
@property
def name(self) -> str:
return REQUIRES_PYTHON_IDENTIFIER
@property
def version(self) -> Version:
return self._version
def format_for_error(self) -> str:
return f"Python {self.version}"
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
return ()
def get_install_requirement(self) -> InstallRequirement | None:
return None

View File

@@ -0,0 +1,856 @@
from __future__ import annotations
import contextlib
import functools
import logging
from collections.abc import Iterable, Iterator, Mapping, Sequence
from typing import (
TYPE_CHECKING,
Callable,
NamedTuple,
Protocol,
TypeVar,
cast,
)
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import InvalidVersion, Version
from pip._vendor.resolvelib import ResolutionImpossible
from pip._internal.cache import CacheEntry, WheelCache
from pip._internal.exceptions import (
DistributionNotFound,
InstallationError,
InvalidInstalledPackage,
MetadataInconsistent,
MetadataInvalid,
UnsupportedPythonVersion,
UnsupportedWheel,
)
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution, get_default_environment
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import (
install_req_drop_extras,
install_req_from_link_and_ireq,
)
from pip._internal.req.req_install import (
InstallRequirement,
check_invalid_constraint_type,
)
from pip._internal.resolution.base import InstallRequirementProvider
from pip._internal.utils.compatibility_tags import get_supported
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.packaging import get_requirement
from pip._internal.utils.virtualenv import running_under_virtualenv
from .base import Candidate, Constraint, Requirement
from .candidates import (
AlreadyInstalledCandidate,
BaseCandidate,
EditableCandidate,
ExtrasCandidate,
LinkCandidate,
RequiresPythonCandidate,
as_base_candidate,
)
from .found_candidates import FoundCandidates, IndexCandidateInfo
from .requirements import (
ExplicitRequirement,
RequiresPythonRequirement,
SpecifierRequirement,
SpecifierWithoutExtrasRequirement,
UnsatisfiableRequirement,
)
if TYPE_CHECKING:
class ConflictCause(Protocol):
requirement: RequiresPythonRequirement
parent: Candidate
logger = logging.getLogger(__name__)
C = TypeVar("C")
Cache = dict[Link, C]
class CollectedRootRequirements(NamedTuple):
requirements: list[Requirement]
constraints: dict[str, Constraint]
user_requested: dict[str, int]
class Factory:
def __init__(
self,
finder: PackageFinder,
preparer: RequirementPreparer,
make_install_req: InstallRequirementProvider,
wheel_cache: WheelCache | None,
use_user_site: bool,
force_reinstall: bool,
ignore_installed: bool,
ignore_requires_python: bool,
py_version_info: tuple[int, ...] | None = None,
) -> None:
self._finder = finder
self.preparer = preparer
self._wheel_cache = wheel_cache
self._python_candidate = RequiresPythonCandidate(py_version_info)
self._make_install_req_from_spec = make_install_req
self._use_user_site = use_user_site
self._force_reinstall = force_reinstall
self._ignore_requires_python = ignore_requires_python
self._build_failures: Cache[InstallationError] = {}
self._link_candidate_cache: Cache[LinkCandidate] = {}
self._editable_candidate_cache: Cache[EditableCandidate] = {}
self._installed_candidate_cache: dict[str, AlreadyInstalledCandidate] = {}
self._extras_candidate_cache: dict[
tuple[int, frozenset[NormalizedName]], ExtrasCandidate
] = {}
self._supported_tags_cache = get_supported()
if not ignore_installed:
env = get_default_environment()
self._installed_dists = {
dist.canonical_name: dist
for dist in env.iter_installed_distributions(local_only=False)
}
else:
self._installed_dists = {}
@property
def force_reinstall(self) -> bool:
return self._force_reinstall
def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None:
if not link.is_wheel:
return
wheel = Wheel(link.filename)
if wheel.supported(self._finder.target_python.get_unsorted_tags()):
return
msg = f"{link.filename} is not a supported wheel on this platform."
raise UnsupportedWheel(msg)
def _make_extras_candidate(
self,
base: BaseCandidate,
extras: frozenset[str],
*,
comes_from: InstallRequirement | None = None,
) -> ExtrasCandidate:
cache_key = (id(base), frozenset(canonicalize_name(e) for e in extras))
try:
candidate = self._extras_candidate_cache[cache_key]
except KeyError:
candidate = ExtrasCandidate(base, extras, comes_from=comes_from)
self._extras_candidate_cache[cache_key] = candidate
return candidate
def _make_candidate_from_dist(
self,
dist: BaseDistribution,
extras: frozenset[str],
template: InstallRequirement,
) -> Candidate:
try:
base = self._installed_candidate_cache[dist.canonical_name]
except KeyError:
base = AlreadyInstalledCandidate(dist, template, factory=self)
self._installed_candidate_cache[dist.canonical_name] = base
if not extras:
return base
return self._make_extras_candidate(base, extras, comes_from=template)
def _make_candidate_from_link(
self,
link: Link,
extras: frozenset[str],
template: InstallRequirement,
name: NormalizedName | None,
version: Version | None,
) -> Candidate | None:
base: BaseCandidate | None = self._make_base_candidate_from_link(
link, template, name, version
)
if not extras or base is None:
return base
return self._make_extras_candidate(base, extras, comes_from=template)
def _make_base_candidate_from_link(
self,
link: Link,
template: InstallRequirement,
name: NormalizedName | None,
version: Version | None,
) -> BaseCandidate | None:
# TODO: Check already installed candidate, and use it if the link and
# editable flag match.
if link in self._build_failures:
# We already tried this candidate before, and it does not build.
# Don't bother trying again.
return None
if template.editable:
if link not in self._editable_candidate_cache:
try:
self._editable_candidate_cache[link] = EditableCandidate(
link,
template,
factory=self,
name=name,
version=version,
)
except (MetadataInconsistent, MetadataInvalid) as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
link,
e,
extra={"markup": True},
)
self._build_failures[link] = e
return None
return self._editable_candidate_cache[link]
else:
if link not in self._link_candidate_cache:
try:
self._link_candidate_cache[link] = LinkCandidate(
link,
template,
factory=self,
name=name,
version=version,
)
except MetadataInconsistent as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
link,
e,
extra={"markup": True},
)
self._build_failures[link] = e
return None
return self._link_candidate_cache[link]
def _iter_found_candidates(
self,
ireqs: Sequence[InstallRequirement],
specifier: SpecifierSet,
hashes: Hashes,
prefers_installed: bool,
incompatible_ids: set[int],
) -> Iterable[Candidate]:
if not ireqs:
return ()
# The InstallRequirement implementation requires us to give it a
# "template". Here we just choose the first requirement to represent
# all of them.
# Hopefully the Project model can correct this mismatch in the future.
template = ireqs[0]
assert template.req, "Candidates found on index must be PEP 508"
name = canonicalize_name(template.req.name)
extras: frozenset[str] = frozenset()
for ireq in ireqs:
assert ireq.req, "Candidates found on index must be PEP 508"
specifier &= ireq.req.specifier
hashes &= ireq.hashes(trust_internet=False)
extras |= frozenset(ireq.extras)
def _get_installed_candidate() -> Candidate | None:
"""Get the candidate for the currently-installed version."""
# If --force-reinstall is set, we want the version from the index
# instead, so we "pretend" there is nothing installed.
if self._force_reinstall:
return None
try:
installed_dist = self._installed_dists[name]
except KeyError:
return None
try:
# Don't use the installed distribution if its version
# does not fit the current dependency graph.
if not specifier.contains(installed_dist.version, prereleases=True):
return None
except InvalidVersion as e:
raise InvalidInstalledPackage(dist=installed_dist, invalid_exc=e)
candidate = self._make_candidate_from_dist(
dist=installed_dist,
extras=extras,
template=template,
)
# The candidate is a known incompatibility. Don't use it.
if id(candidate) in incompatible_ids:
return None
return candidate
def iter_index_candidate_infos() -> Iterator[IndexCandidateInfo]:
result = self._finder.find_best_candidate(
project_name=name,
specifier=specifier,
hashes=hashes,
)
icans = result.applicable_candidates
# PEP 592: Yanked releases are ignored unless the specifier
# explicitly pins a version (via '==' or '===') that can be
# solely satisfied by a yanked release.
all_yanked = all(ican.link.is_yanked for ican in icans)
def is_pinned(specifier: SpecifierSet) -> bool:
for sp in specifier:
if sp.operator == "===":
return True
if sp.operator != "==":
continue
if sp.version.endswith(".*"):
continue
return True
return False
pinned = is_pinned(specifier)
# PackageFinder returns earlier versions first, so we reverse.
for ican in reversed(icans):
if not (all_yanked and pinned) and ican.link.is_yanked:
continue
func = functools.partial(
self._make_candidate_from_link,
link=ican.link,
extras=extras,
template=template,
name=name,
version=ican.version,
)
yield ican.version, func
return FoundCandidates(
iter_index_candidate_infos,
_get_installed_candidate(),
prefers_installed,
incompatible_ids,
)
def _iter_explicit_candidates_from_base(
self,
base_requirements: Iterable[Requirement],
extras: frozenset[str],
) -> Iterator[Candidate]:
"""Produce explicit candidates from the base given an extra-ed package.
:param base_requirements: Requirements known to the resolver. The
requirements are guaranteed to not have extras.
:param extras: The extras to inject into the explicit requirements'
candidates.
"""
for req in base_requirements:
lookup_cand, _ = req.get_candidate_lookup()
if lookup_cand is None: # Not explicit.
continue
# We've stripped extras from the identifier, and should always
# get a BaseCandidate here, unless there's a bug elsewhere.
base_cand = as_base_candidate(lookup_cand)
assert base_cand is not None, "no extras here"
yield self._make_extras_candidate(base_cand, extras)
def _iter_candidates_from_constraints(
self,
identifier: str,
constraint: Constraint,
template: InstallRequirement,
) -> Iterator[Candidate]:
"""Produce explicit candidates from constraints.
This creates "fake" InstallRequirement objects that are basically clones
of what "should" be the template, but with original_link set to link.
"""
for link in constraint.links:
self._fail_if_link_is_unsupported_wheel(link)
candidate = self._make_base_candidate_from_link(
link,
template=install_req_from_link_and_ireq(link, template),
name=canonicalize_name(identifier),
version=None,
)
if candidate:
yield candidate
def find_candidates(
self,
identifier: str,
requirements: Mapping[str, Iterable[Requirement]],
incompatibilities: Mapping[str, Iterator[Candidate]],
constraint: Constraint,
prefers_installed: bool,
is_satisfied_by: Callable[[Requirement, Candidate], bool],
) -> Iterable[Candidate]:
# Collect basic lookup information from the requirements.
explicit_candidates: set[Candidate] = set()
ireqs: list[InstallRequirement] = []
for req in requirements[identifier]:
cand, ireq = req.get_candidate_lookup()
if cand is not None:
explicit_candidates.add(cand)
if ireq is not None:
ireqs.append(ireq)
# If the current identifier contains extras, add requires and explicit
# candidates from entries from extra-less identifier.
with contextlib.suppress(InvalidRequirement):
parsed_requirement = get_requirement(identifier)
if parsed_requirement.name != identifier:
explicit_candidates.update(
self._iter_explicit_candidates_from_base(
requirements.get(parsed_requirement.name, ()),
frozenset(parsed_requirement.extras),
),
)
for req in requirements.get(parsed_requirement.name, []):
_, ireq = req.get_candidate_lookup()
if ireq is not None:
ireqs.append(ireq)
# Add explicit candidates from constraints. We only do this if there are
# known ireqs, which represent requirements not already explicit. If
# there are no ireqs, we're constraining already-explicit requirements,
# which is handled later when we return the explicit candidates.
if ireqs:
try:
explicit_candidates.update(
self._iter_candidates_from_constraints(
identifier,
constraint,
template=ireqs[0],
),
)
except UnsupportedWheel:
# If we're constrained to install a wheel incompatible with the
# target architecture, no candidates will ever be valid.
return ()
# Since we cache all the candidates, incompatibility identification
# can be made quicker by comparing only the id() values.
incompat_ids = {id(c) for c in incompatibilities.get(identifier, ())}
# If none of the requirements want an explicit candidate, we can ask
# the finder for candidates.
if not explicit_candidates:
return self._iter_found_candidates(
ireqs,
constraint.specifier,
constraint.hashes,
prefers_installed,
incompat_ids,
)
return (
c
for c in explicit_candidates
if id(c) not in incompat_ids
and constraint.is_satisfied_by(c)
and all(is_satisfied_by(req, c) for req in requirements[identifier])
)
def _make_requirements_from_install_req(
self, ireq: InstallRequirement, requested_extras: Iterable[str]
) -> Iterator[Requirement]:
"""
Returns requirement objects associated with the given InstallRequirement. In
most cases this will be a single object but the following special cases exist:
- the InstallRequirement has markers that do not apply -> result is empty
- the InstallRequirement has both a constraint (or link) and extras
-> result is split in two requirement objects: one with the constraint
(or link) and one with the extra. This allows centralized constraint
handling for the base, resulting in fewer candidate rejections.
"""
if not ireq.match_markers(requested_extras):
logger.info(
"Ignoring %s: markers '%s' don't match your environment",
ireq.name,
ireq.markers,
)
elif not ireq.link:
if ireq.extras and ireq.req is not None and ireq.req.specifier:
yield SpecifierWithoutExtrasRequirement(ireq)
yield SpecifierRequirement(ireq)
else:
self._fail_if_link_is_unsupported_wheel(ireq.link)
# Always make the link candidate for the base requirement to make it
# available to `find_candidates` for explicit candidate lookup for any
# set of extras.
# The extras are required separately via a second requirement.
cand = self._make_base_candidate_from_link(
ireq.link,
template=install_req_drop_extras(ireq) if ireq.extras else ireq,
name=canonicalize_name(ireq.name) if ireq.name else None,
version=None,
)
if cand is None:
# There's no way we can satisfy a URL requirement if the underlying
# candidate fails to build. An unnamed URL must be user-supplied, so
# we fail eagerly. If the URL is named, an unsatisfiable requirement
# can make the resolver do the right thing, either backtrack (and
# maybe find some other requirement that's buildable) or raise a
# ResolutionImpossible eventually.
if not ireq.name:
raise self._build_failures[ireq.link]
yield UnsatisfiableRequirement(canonicalize_name(ireq.name))
else:
# require the base from the link
yield self.make_requirement_from_candidate(cand)
if ireq.extras:
# require the extras on top of the base candidate
yield self.make_requirement_from_candidate(
self._make_extras_candidate(cand, frozenset(ireq.extras))
)
def collect_root_requirements(
self, root_ireqs: list[InstallRequirement]
) -> CollectedRootRequirements:
collected = CollectedRootRequirements([], {}, {})
for i, ireq in enumerate(root_ireqs):
if ireq.constraint:
# Ensure we only accept valid constraints
problem = check_invalid_constraint_type(ireq)
if problem:
raise InstallationError(problem)
if not ireq.match_markers():
continue
assert ireq.name, "Constraint must be named"
name = canonicalize_name(ireq.name)
if name in collected.constraints:
collected.constraints[name] &= ireq
else:
collected.constraints[name] = Constraint.from_ireq(ireq)
else:
reqs = list(
self._make_requirements_from_install_req(
ireq,
requested_extras=(),
)
)
if not reqs:
continue
template = reqs[0]
if ireq.user_supplied and template.name not in collected.user_requested:
collected.user_requested[template.name] = i
collected.requirements.extend(reqs)
# Put requirements with extras at the end of the root requires. This does not
# affect resolvelib's picking preference but it does affect its initial criteria
# population: by putting extras at the end we enable the candidate finder to
# present resolvelib with a smaller set of candidates to resolvelib, already
# taking into account any non-transient constraints on the associated base. This
# means resolvelib will have fewer candidates to visit and reject.
# Python's list sort is stable, meaning relative order is kept for objects with
# the same key.
collected.requirements.sort(key=lambda r: r.name != r.project_name)
return collected
def make_requirement_from_candidate(
self, candidate: Candidate
) -> ExplicitRequirement:
return ExplicitRequirement(candidate)
def make_requirements_from_spec(
self,
specifier: str,
comes_from: InstallRequirement | None,
requested_extras: Iterable[str] = (),
) -> Iterator[Requirement]:
"""
Returns requirement objects associated with the given specifier. In most cases
this will be a single object but the following special cases exist:
- the specifier has markers that do not apply -> result is empty
- the specifier has both a constraint and extras -> result is split
in two requirement objects: one with the constraint and one with the
extra. This allows centralized constraint handling for the base,
resulting in fewer candidate rejections.
"""
ireq = self._make_install_req_from_spec(specifier, comes_from)
return self._make_requirements_from_install_req(ireq, requested_extras)
def make_requires_python_requirement(
self,
specifier: SpecifierSet,
) -> Requirement | None:
if self._ignore_requires_python:
return None
# Don't bother creating a dependency for an empty Requires-Python.
if not str(specifier):
return None
return RequiresPythonRequirement(specifier, self._python_candidate)
def get_wheel_cache_entry(self, link: Link, name: str | None) -> CacheEntry | None:
"""Look up the link in the wheel cache.
If ``preparer.require_hashes`` is True, don't use the wheel cache,
because cached wheels, always built locally, have different hashes
than the files downloaded from the index server and thus throw false
hash mismatches. Furthermore, cached wheels at present have
nondeterministic contents due to file modification times.
"""
if self._wheel_cache is None:
return None
return self._wheel_cache.get_cache_entry(
link=link,
package_name=name,
supported_tags=self._supported_tags_cache,
)
def get_dist_to_uninstall(self, candidate: Candidate) -> BaseDistribution | None:
# TODO: Are there more cases this needs to return True? Editable?
dist = self._installed_dists.get(candidate.project_name)
if dist is None: # Not installed, no uninstallation required.
return None
# We're installing into global site. The current installation must
# be uninstalled, no matter it's in global or user site, because the
# user site installation has precedence over global.
if not self._use_user_site:
return dist
# We're installing into user site. Remove the user site installation.
if dist.in_usersite:
return dist
# We're installing into user site, but the installed incompatible
# package is in global site. We can't uninstall that, and would let
# the new user installation to "shadow" it. But shadowing won't work
# in virtual environments, so we error out.
if running_under_virtualenv() and dist.in_site_packages:
message = (
f"Will not install to the user site because it will lack "
f"sys.path precedence to {dist.raw_name} in {dist.location}"
)
raise InstallationError(message)
return None
def _report_requires_python_error(
self, causes: Sequence[ConflictCause]
) -> UnsupportedPythonVersion:
assert causes, "Requires-Python error reported with no cause"
version = self._python_candidate.version
if len(causes) == 1:
specifier = str(causes[0].requirement.specifier)
message = (
f"Package {causes[0].parent.name!r} requires a different "
f"Python: {version} not in {specifier!r}"
)
return UnsupportedPythonVersion(message)
message = f"Packages require a different Python. {version} not in:"
for cause in causes:
package = cause.parent.format_for_error()
specifier = str(cause.requirement.specifier)
message += f"\n{specifier!r} (required by {package})"
return UnsupportedPythonVersion(message)
def _report_single_requirement_conflict(
self, req: Requirement, parent: Candidate | None
) -> DistributionNotFound:
if parent is None:
req_disp = str(req)
else:
req_disp = f"{req} (from {parent.name})"
cands = self._finder.find_all_candidates(req.project_name)
skipped_by_requires_python = self._finder.requires_python_skipped_reasons()
versions_set: set[Version] = set()
yanked_versions_set: set[Version] = set()
for c in cands:
is_yanked = c.link.is_yanked if c.link else False
if is_yanked:
yanked_versions_set.add(c.version)
else:
versions_set.add(c.version)
versions = [str(v) for v in sorted(versions_set)]
yanked_versions = [str(v) for v in sorted(yanked_versions_set)]
if yanked_versions:
# Saying "version X is yanked" isn't entirely accurate.
# https://github.com/pypa/pip/issues/11745#issuecomment-1402805842
logger.critical(
"Ignored the following yanked versions: %s",
", ".join(yanked_versions) or "none",
)
if skipped_by_requires_python:
logger.critical(
"Ignored the following versions that require a different python "
"version: %s",
"; ".join(skipped_by_requires_python) or "none",
)
# Check if only final releases are allowed for this package
version_type = "version"
if self._finder.release_control is not None:
allows_pre = self._finder.release_control.allows_prereleases(
canonicalize_name(req.project_name)
)
if allows_pre is False:
version_type = "final version"
logger.critical(
"Could not find a %s that satisfies the requirement %s "
"(from versions: %s)",
version_type,
req_disp,
", ".join(versions) or "none",
)
if str(req) == "requirements.txt":
logger.info(
"HINT: You are attempting to install a package literally "
'named "requirements.txt" (which cannot exist). Consider '
"using the '-r' flag to install the packages listed in "
"requirements.txt"
)
return DistributionNotFound(f"No matching distribution found for {req}")
def _has_any_candidates(self, project_name: str) -> bool:
"""
Check if there are any candidates available for the project name.
"""
return any(
self.find_candidates(
project_name,
requirements={project_name: []},
incompatibilities={},
constraint=Constraint.empty(),
prefers_installed=True,
is_satisfied_by=lambda r, c: True,
)
)
def get_installation_error(
self,
e: ResolutionImpossible[Requirement, Candidate],
constraints: dict[str, Constraint],
) -> InstallationError:
assert e.causes, "Installation error reported with no cause"
# If one of the things we can't solve is "we need Python X.Y",
# that is what we report.
requires_python_causes = [
cause
for cause in e.causes
if isinstance(cause.requirement, RequiresPythonRequirement)
and not cause.requirement.is_satisfied_by(self._python_candidate)
]
if requires_python_causes:
# The comprehension above makes sure all Requirement instances are
# RequiresPythonRequirement, so let's cast for convenience.
return self._report_requires_python_error(
cast("Sequence[ConflictCause]", requires_python_causes),
)
# Otherwise, we have a set of causes which can't all be satisfied
# at once.
# 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 = next(iter(e.causes))
if req.name not in constraints:
return self._report_single_requirement_conflict(req, parent)
# OK, we now have a list of requirements that can't all be
# satisfied at once.
# A couple of formatting helpers
def text_join(parts: list[str]) -> str:
if len(parts) == 1:
return parts[0]
return ", ".join(parts[:-1]) + " and " + parts[-1]
def describe_trigger(parent: Candidate) -> str:
ireq = parent.get_install_requirement()
if not ireq or not ireq.comes_from:
return f"{parent.name}=={parent.version}"
if isinstance(ireq.comes_from, InstallRequirement):
return str(ireq.comes_from.name)
return str(ireq.comes_from)
triggers = set()
for req, parent in e.causes:
if parent is None:
# This is a root requirement, so we can report it directly
trigger = req.format_for_error()
else:
trigger = describe_trigger(parent)
triggers.add(trigger)
if triggers:
info = text_join(sorted(triggers))
else:
info = "the requested packages"
msg = (
f"Cannot install {info} because these package versions "
"have conflicting dependencies."
)
logger.critical(msg)
msg = "\nThe conflict is caused by:"
relevant_constraints = set()
for req, parent in e.causes:
if req.name in constraints:
relevant_constraints.add(req.name)
msg = msg + "\n "
if parent:
msg = msg + f"{parent.name} {parent.version} depends on "
else:
msg = msg + "The user requested "
msg = msg + req.format_for_error()
for key in relevant_constraints:
spec = constraints[key].specifier
msg += f"\n The user requested (constraint) {key}{spec}"
# Check for causes that had no candidates
causes = set()
for req, _ in e.causes:
causes.add(req.name)
no_candidates = {c for c in causes if not self._has_any_candidates(c)}
if no_candidates:
msg = (
msg
+ "\n\n"
+ "Additionally, some packages in these conflicts have no "
+ "matching distributions available for your environment:"
+ "\n "
+ "\n ".join(sorted(no_candidates))
)
msg = (
msg
+ "\n\n"
+ "To fix this you could try to:\n"
+ "1. loosen the range of package versions you've specified\n"
+ "2. remove package versions to allow pip to attempt to solve "
+ "the dependency conflict\n"
)
logger.info(msg)
return DistributionNotFound(
"ResolutionImpossible: for help visit "
"https://pip.pypa.io/en/latest/topics/dependency-resolution/"
"#dealing-with-dependency-conflicts"
)

View File

@@ -0,0 +1,166 @@
"""Utilities to lazily create and visit candidates found.
Creating and visiting a candidate is a *very* costly operation. It involves
fetching, extracting, potentially building modules from source, and verifying
distribution metadata. It is therefore crucial for performance to keep
everything here lazy all the way down, so we only touch candidates that we
absolutely need, and not "download the world" when we only need one version of
something.
"""
from __future__ import annotations
import logging
from collections.abc import Iterator, Sequence
from typing import Any, Callable, Optional
from pip._vendor.packaging.version import _BaseVersion
from pip._internal.exceptions import MetadataInvalid
from .base import Candidate
logger = logging.getLogger(__name__)
IndexCandidateInfo = tuple[_BaseVersion, Callable[[], Optional[Candidate]]]
def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
"""Iterator for ``FoundCandidates``.
This iterator is used when the package is not already installed. Candidates
from index come later in their normal ordering.
"""
versions_found: set[_BaseVersion] = set()
for version, func in infos:
if version in versions_found:
continue
try:
candidate = func()
except MetadataInvalid as e:
logger.warning(
"Ignoring version %s of %s since it has invalid metadata:\n"
"%s\n"
"Please use pip<24.1 if you need to use this version.",
version,
e.ireq.name,
e,
)
# Mark version as found to avoid trying other candidates with the same
# version, since they most likely have invalid metadata as well.
versions_found.add(version)
else:
if candidate is None:
continue
yield candidate
versions_found.add(version)
def _iter_built_with_prepended(
installed: Candidate, infos: Iterator[IndexCandidateInfo]
) -> Iterator[Candidate]:
"""Iterator for ``FoundCandidates``.
This iterator is used when the resolver prefers the already-installed
candidate and NOT to upgrade. The installed candidate is therefore
always yielded first, and candidates from index come later in their
normal ordering, except skipped when the version is already installed.
"""
yield installed
versions_found: set[_BaseVersion] = {installed.version}
for version, func in infos:
if version in versions_found:
continue
candidate = func()
if candidate is None:
continue
yield candidate
versions_found.add(version)
def _iter_built_with_inserted(
installed: Candidate, infos: Iterator[IndexCandidateInfo]
) -> Iterator[Candidate]:
"""Iterator for ``FoundCandidates``.
This iterator is used when the resolver prefers to upgrade an
already-installed package. Candidates from index are returned in their
normal ordering, except replaced when the version is already installed.
The implementation iterates through and yields other candidates, inserting
the installed candidate exactly once before we start yielding older or
equivalent candidates, or after all other candidates if they are all newer.
"""
versions_found: set[_BaseVersion] = set()
for version, func in infos:
if version in versions_found:
continue
# If the installed candidate is better, yield it first.
if installed.version >= version:
yield installed
versions_found.add(installed.version)
candidate = func()
if candidate is None:
continue
yield candidate
versions_found.add(version)
# If the installed candidate is older than all other candidates.
if installed.version not in versions_found:
yield installed
class FoundCandidates(Sequence[Candidate]):
"""A lazy sequence to provide candidates to the resolver.
The intended usage is to return this from `find_matches()` so the resolver
can iterate through the sequence multiple times, but only access the index
page when remote packages are actually needed. This improve performances
when suitable candidates are already installed on disk.
"""
def __init__(
self,
get_infos: Callable[[], Iterator[IndexCandidateInfo]],
installed: Candidate | None,
prefers_installed: bool,
incompatible_ids: set[int],
):
self._get_infos = get_infos
self._installed = installed
self._prefers_installed = prefers_installed
self._incompatible_ids = incompatible_ids
self._bool: bool | None = None
def __getitem__(self, index: Any) -> Any:
# Implemented to satisfy the ABC check. This is not needed by the
# resolver, and should not be used by the provider either (for
# performance reasons).
raise NotImplementedError("don't do this")
def __iter__(self) -> Iterator[Candidate]:
infos = self._get_infos()
if not self._installed:
iterator = _iter_built(infos)
elif self._prefers_installed:
iterator = _iter_built_with_prepended(self._installed, infos)
else:
iterator = _iter_built_with_inserted(self._installed, infos)
return (c for c in iterator if id(c) not in self._incompatible_ids)
def __len__(self) -> int:
# Implemented to satisfy the ABC check. This is not needed by the
# resolver, and should not be used by the provider either (for
# performance reasons).
raise NotImplementedError("don't do this")
def __bool__(self) -> bool:
if self._bool is not None:
return self._bool
if self._prefers_installed and self._installed:
self._bool = True
return True
self._bool = any(self)
return self._bool