Voice et bot modif

This commit is contained in:
pi 2026-06-16 17:09:34 +00:00
parent 189d56026b
commit 7333a22bcd
10774 changed files with 634644 additions and 933308 deletions

View file

@ -1,4 +1,4 @@
from typing import Callable, Optional
from typing import Callable, List, Optional
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_set import RequirementSet
@ -10,11 +10,11 @@ InstallRequirementProvider = Callable[
class BaseResolver:
def resolve(
self, root_reqs: list[InstallRequirement], check_supported_wheels: bool
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
) -> RequirementSet:
raise NotImplementedError()
def get_installation_order(
self, req_set: RequirementSet
) -> list[InstallRequirement]:
) -> List[InstallRequirement]:
raise NotImplementedError()

View file

@ -10,14 +10,14 @@ for sub-dependencies
a. "first found, wins" (where the order is breadth first)
"""
from __future__ import annotations
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import logging
import sys
from collections import defaultdict
from collections.abc import Iterable
from itertools import chain
from typing import Optional
from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.requirements import Requirement
@ -52,12 +52,12 @@ from pip._internal.utils.packaging import check_requires_python
logger = logging.getLogger(__name__)
DiscoveredDependencies = defaultdict[Optional[str], list[InstallRequirement]]
DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
def _check_dist_requires_python(
dist: BaseDistribution,
version_info: tuple[int, int, int],
version_info: Tuple[int, int, int],
ignore_requires_python: bool = False,
) -> None:
"""
@ -104,8 +104,9 @@ def _check_dist_requires_python(
return
raise UnsupportedPythonVersion(
f"Package {dist.raw_name!r} requires a different Python: "
f"{version} not in {requires_python!r}"
"Package {!r} requires a different Python: {} not in {!r}".format(
dist.raw_name, version, requires_python
)
)
@ -120,7 +121,7 @@ class Resolver(BaseResolver):
self,
preparer: RequirementPreparer,
finder: PackageFinder,
wheel_cache: WheelCache | None,
wheel_cache: Optional[WheelCache],
make_install_req: InstallRequirementProvider,
use_user_site: bool,
ignore_dependencies: bool,
@ -128,7 +129,7 @@ class Resolver(BaseResolver):
ignore_requires_python: bool,
force_reinstall: bool,
upgrade_strategy: str,
py_version_info: tuple[int, ...] | None = None,
py_version_info: Optional[Tuple[int, ...]] = None,
) -> None:
super().__init__()
assert upgrade_strategy in self._allowed_strategies
@ -155,7 +156,7 @@ class Resolver(BaseResolver):
self._discovered_dependencies: DiscoveredDependencies = defaultdict(list)
def resolve(
self, root_reqs: list[InstallRequirement], check_supported_wheels: bool
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
) -> RequirementSet:
"""Resolve what operations need to be done
@ -177,7 +178,7 @@ class Resolver(BaseResolver):
# exceptions cannot be checked ahead of time, because
# _populate_link() needs to be called before we can make decisions
# based on link type.
discovered_reqs: list[InstallRequirement] = []
discovered_reqs: List[InstallRequirement] = []
hash_errors = HashErrors()
for req in chain(requirement_set.all_requirements, discovered_reqs):
try:
@ -195,9 +196,9 @@ class Resolver(BaseResolver):
self,
requirement_set: RequirementSet,
install_req: InstallRequirement,
parent_req_name: str | None = None,
extras_requested: Iterable[str] | None = None,
) -> tuple[list[InstallRequirement], InstallRequirement | None]:
parent_req_name: Optional[str] = None,
extras_requested: Optional[Iterable[str]] = None,
) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
"""Add install_req as a requirement to install.
:param parent_req_name: The name of the requirement that needed this
@ -245,9 +246,9 @@ class Resolver(BaseResolver):
return [install_req], None
try:
existing_req: InstallRequirement | None = requirement_set.get_requirement(
install_req.name
)
existing_req: Optional[
InstallRequirement
] = requirement_set.get_requirement(install_req.name)
except KeyError:
existing_req = None
@ -262,8 +263,9 @@ class Resolver(BaseResolver):
)
if has_conflicting_requirement:
raise InstallationError(
f"Double requirement given: {install_req} "
f"(already in {existing_req}, name={install_req.name!r})"
"Double requirement given: {} (already in {}, name={!r})".format(
install_req, existing_req, install_req.name
)
)
# When no existing requirement exists, add the requirement as a
@ -321,12 +323,13 @@ class Resolver(BaseResolver):
"""
# Don't uninstall the conflict if doing a user install and the
# conflict is not a user install.
assert req.satisfied_by is not None
if not self.use_user_site or req.satisfied_by.in_usersite:
req.should_reinstall = True
req.satisfied_by = None
def _check_skip_installed(self, req_to_install: InstallRequirement) -> str | None:
def _check_skip_installed(
self, req_to_install: InstallRequirement
) -> Optional[str]:
"""Check if req_to_install should be skipped.
This will check if the req is installed, and whether we should upgrade
@ -378,7 +381,7 @@ class Resolver(BaseResolver):
self._set_req_to_reinstall(req_to_install)
return None
def _find_requirement_link(self, req: InstallRequirement) -> Link | None:
def _find_requirement_link(self, req: InstallRequirement) -> Optional[Link]:
upgrade = self._is_upgrade_allowed(req)
best_candidate = self.finder.find_requirement(req, upgrade)
if not best_candidate:
@ -418,8 +421,6 @@ class Resolver(BaseResolver):
if self.wheel_cache is None or self.preparer.require_hashes:
return
assert req.link is not None, "_find_requirement_link unexpectedly returned None"
cache_entry = self.wheel_cache.get_cache_entry(
link=req.link,
package_name=req.name,
@ -489,7 +490,7 @@ class Resolver(BaseResolver):
self,
requirement_set: RequirementSet,
req_to_install: InstallRequirement,
) -> list[InstallRequirement]:
) -> List[InstallRequirement]:
"""Prepare a single requirements file.
:return: A list of additional InstallRequirements to also install.
@ -512,7 +513,7 @@ class Resolver(BaseResolver):
ignore_requires_python=self.ignore_requires_python,
)
more_reqs: list[InstallRequirement] = []
more_reqs: List[InstallRequirement] = []
def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None:
# This idiosyncratically converts the Requirement to str and let
@ -533,7 +534,6 @@ class Resolver(BaseResolver):
with indent_log():
# We add req_to_install before its dependencies, so that we
# can refer to it when adding dependencies.
assert req_to_install.name is not None
if not requirement_set.has_requirement(req_to_install.name):
# 'unnamed' requirements will get added here
# 'unnamed' requirements can only come from being directly
@ -570,7 +570,7 @@ class Resolver(BaseResolver):
def get_installation_order(
self, req_set: RequirementSet
) -> list[InstallRequirement]:
) -> List[InstallRequirement]:
"""Create the installation order.
The installation order is topological - requirements are installed
@ -581,7 +581,7 @@ class Resolver(BaseResolver):
# installs the user specified things in the order given, except when
# dependencies must come earlier to achieve topological order.
order = []
ordered_reqs: set[InstallRequirement] = set()
ordered_reqs: Set[InstallRequirement] = set()
def schedule(req: InstallRequirement) -> None:
if req.satisfied_by or req in ordered_reqs:

View file

@ -1,46 +1,45 @@
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Optional
from typing import FrozenSet, Iterable, Optional, Tuple, Union
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import LegacyVersion, 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]]
CandidateLookup = Tuple[Optional["Candidate"], Optional[InstallRequirement]]
CandidateVersion = Union[LegacyVersion, Version]
def format_name(project: NormalizedName, extras: frozenset[NormalizedName]) -> str:
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]
def __init__(
self, specifier: SpecifierSet, hashes: Hashes, links: FrozenSet[Link]
) -> None:
self.specifier = specifier
self.hashes = hashes
self.links = links
@classmethod
def empty(cls) -> Constraint:
def empty(cls) -> "Constraint":
return Constraint(SpecifierSet(), Hashes(), frozenset())
@classmethod
def from_ireq(cls, ireq: InstallRequirement) -> Constraint:
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:
def __and__(self, other: InstallRequirement) -> "Constraint":
if not isinstance(other, InstallRequirement):
return NotImplemented
specifier = self.specifier & other.specifier
@ -50,7 +49,7 @@ class Constraint:
links = links.union([other.link])
return Constraint(specifier, hashes, links)
def is_satisfied_by(self, candidate: Candidate) -> bool:
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
@ -80,7 +79,7 @@ class Requirement:
"""
raise NotImplementedError("Subclass should override")
def is_satisfied_by(self, candidate: Candidate) -> bool:
def is_satisfied_by(self, candidate: "Candidate") -> bool:
return False
def get_candidate_lookup(self) -> CandidateLookup:
@ -90,7 +89,7 @@ class Requirement:
raise NotImplementedError("Subclass should override")
def _match_link(link: Link, candidate: Candidate) -> bool:
def _match_link(link: Link, candidate: "Candidate") -> bool:
if candidate.source_link:
return links_equivalent(link, candidate.source_link)
return False
@ -117,7 +116,7 @@ class Candidate:
raise NotImplementedError("Override in subclass")
@property
def version(self) -> Version:
def version(self) -> CandidateVersion:
raise NotImplementedError("Override in subclass")
@property
@ -129,13 +128,13 @@ class Candidate:
raise NotImplementedError("Override in subclass")
@property
def source_link(self) -> Link | None:
def source_link(self) -> Optional[Link]:
raise NotImplementedError("Override in subclass")
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
raise NotImplementedError("Override in subclass")
def get_install_requirement(self) -> InstallRequirement | None:
def get_install_requirement(self) -> Optional[InstallRequirement]:
raise NotImplementedError("Override in subclass")
def format_for_error(self) -> str:

View file

@ -1,21 +1,14 @@
from __future__ import annotations
import logging
import sys
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Union, cast
from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, 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
@ -28,7 +21,7 @@ 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
from .base import Candidate, CandidateVersion, Requirement, format_name
if TYPE_CHECKING:
from .factory import Factory
@ -45,7 +38,7 @@ BaseCandidate = Union[
REQUIRES_PYTHON_IDENTIFIER = cast(NormalizedName, "<Python from Requires-Python>")
def as_base_candidate(candidate: Candidate) -> BaseCandidate | None:
def as_base_candidate(candidate: Candidate) -> Optional[BaseCandidate]:
"""The runtime version of BaseCandidate."""
base_candidate_classes = (
AlreadyInstalledCandidate,
@ -69,8 +62,10 @@ def make_install_req_from_link(
line,
user_supplied=template.user_supplied,
comes_from=template.comes_from,
use_pep517=template.use_pep517,
isolated=template.isolated,
constraint=template.constraint,
global_options=template.global_options,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
@ -84,17 +79,15 @@ 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,
link.url,
user_supplied=template.user_supplied,
comes_from=template.comes_from,
use_pep517=template.use_pep517,
isolated=template.isolated,
constraint=template.constraint,
permit_editable_wheels=template.permit_editable_wheels,
global_options=template.global_options,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
@ -115,8 +108,10 @@ def _make_install_req_from_dist(
line,
user_supplied=template.user_supplied,
comes_from=template.comes_from,
use_pep517=template.use_pep517,
isolated=template.isolated,
constraint=template.constraint,
global_options=template.global_options,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
@ -148,9 +143,9 @@ class _InstallRequirementBackedCandidate(Candidate):
link: Link,
source_link: Link,
ireq: InstallRequirement,
factory: Factory,
name: NormalizedName | None = None,
version: Version | None = None,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
) -> None:
self._link = link
self._source_link = source_link
@ -159,7 +154,6 @@ class _InstallRequirementBackedCandidate(Candidate):
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}"
@ -168,11 +162,7 @@ class _InstallRequirementBackedCandidate(Candidate):
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
return hash((self.__class__, self._link))
def __eq__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
@ -180,7 +170,7 @@ class _InstallRequirementBackedCandidate(Candidate):
return False
@property
def source_link(self) -> Link | None:
def source_link(self) -> Optional[Link]:
return self._source_link
@property
@ -195,15 +185,16 @@ class _InstallRequirementBackedCandidate(Candidate):
return self.project_name
@property
def version(self) -> Version:
def version(self) -> CandidateVersion:
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})"
return "{} {} (from {})".format(
self.name,
self.version,
self._link.file_path if self._link.is_file else self._link,
)
def _prepare_distribution(self) -> BaseDistribution:
@ -225,13 +216,6 @@ class _InstallRequirementBackedCandidate(Candidate):
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:
@ -243,32 +227,20 @@ class _InstallRequirementBackedCandidate(Candidate):
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,
)
# The output has been presented already, so don't duplicate it.
exc.context = "See above for output."
raise
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)
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
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)
yield self._factory.make_requires_python_requirement(self.dist.requires_python)
def get_install_requirement(self) -> InstallRequirement | None:
def get_install_requirement(self) -> Optional[InstallRequirement]:
return self._ireq
@ -279,9 +251,9 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
self,
link: Link,
template: InstallRequirement,
factory: Factory,
name: NormalizedName | None = None,
version: Version | None = None,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
) -> None:
source_link = link
cache_entry = factory.get_wheel_cache_entry(source_link, name)
@ -292,14 +264,14 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
assert ireq.link == link
if ireq.link.is_wheel and not ireq.link.is_file:
wheel = Wheel(ireq.link.filename)
wheel_name = wheel.name
wheel_name = canonicalize_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}"
assert version == wheel_version, "{!r} != {!r} for wheel {}".format(
version, wheel_version, name
)
if cache_entry is not None:
assert ireq.link.is_wheel
@ -336,9 +308,9 @@ class EditableCandidate(_InstallRequirementBackedCandidate):
self,
link: Link,
template: InstallRequirement,
factory: Factory,
name: NormalizedName | None = None,
version: Version | None = None,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
) -> None:
super().__init__(
link=link,
@ -361,7 +333,7 @@ class AlreadyInstalledCandidate(Candidate):
self,
dist: BaseDistribution,
template: InstallRequirement,
factory: Factory,
factory: "Factory",
) -> None:
self.dist = dist
self._ireq = _make_install_req_from_dist(dist, template)
@ -381,13 +353,13 @@ class AlreadyInstalledCandidate(Candidate):
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))
return hash((self.__class__, self.name, self.version))
def __eq__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.name == other.name and self.version == other.version
return False
@property
def project_name(self) -> NormalizedName:
@ -398,7 +370,7 @@ class AlreadyInstalledCandidate(Candidate):
return self.project_name
@property
def version(self) -> Version:
def version(self) -> CandidateVersion:
if self._version is None:
self._version = self.dist.version
return self._version
@ -410,17 +382,13 @@ class AlreadyInstalledCandidate(Candidate):
def format_for_error(self) -> str:
return f"{self.name} {self.version} (Installed)"
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
if not with_requires:
return
for r in self.dist.iter_dependencies():
yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
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:
def get_install_requirement(self) -> Optional[InstallRequirement]:
return None
@ -452,9 +420,9 @@ class ExtrasCandidate(Candidate):
def __init__(
self,
base: BaseCandidate,
extras: frozenset[str],
extras: FrozenSet[str],
*,
comes_from: InstallRequirement | None = None,
comes_from: Optional[InstallRequirement] = None,
) -> None:
"""
:param comes_from: the InstallRequirement that led to this candidate if it
@ -466,6 +434,14 @@ class ExtrasCandidate(Candidate):
"""
self.base = base
self.extras = frozenset(canonicalize_name(e) for e in extras)
# If any extras are requested in their non-normalized forms, keep track
# of their raw values. This is needed when we look up dependencies
# since PEP 685 has not been implemented for marker-matching, and using
# the non-normalized extra for lookup ensures the user can select a
# non-normalized extra in a package with its non-normalized form.
# TODO: Remove this attribute when packaging is upgraded to support the
# marker comparison logic specified in PEP 685.
self._unnormalized_extras = extras.difference(self.extras)
self._comes_from = comes_from if comes_from is not None else self.base._ireq
def __str__(self) -> str:
@ -493,7 +469,7 @@ class ExtrasCandidate(Candidate):
return format_name(self.base.project_name, self.extras)
@property
def version(self) -> Version:
def version(self) -> CandidateVersion:
return self.base.version
def format_for_error(self) -> str:
@ -510,10 +486,54 @@ class ExtrasCandidate(Candidate):
return self.base.is_editable
@property
def source_link(self) -> Link | None:
def source_link(self) -> Optional[Link]:
return self.base.source_link
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
def _warn_invalid_extras(
self,
requested: FrozenSet[str],
valid: FrozenSet[str],
) -> None:
"""Emit warnings for invalid extras being requested.
This emits a warning for each requested extra that is not in the
candidate's ``Provides-Extra`` list.
"""
invalid_extras_to_warn = frozenset(
extra
for extra in requested
if extra not in valid
# If an extra is requested in an unnormalized form, skip warning
# about the normalized form being missing.
and extra in self.extras
)
if not invalid_extras_to_warn:
return
for extra in sorted(invalid_extras_to_warn):
logger.warning(
"%s %s does not provide the extra '%s'",
self.base.name,
self.version,
extra,
)
def _calculate_valid_requested_extras(self) -> FrozenSet[str]:
"""Get a list of valid extras requested by this candidate.
The user (or upstream dependant) may have specified extras that the
candidate doesn't support. Any unsupported extras are dropped, and each
cause a warning to be logged here.
"""
requested_extras = self.extras.union(self._unnormalized_extras)
valid_extras = frozenset(
extra
for extra in requested_extras
if self.base.dist.is_extra_provided(extra)
)
self._warn_invalid_extras(requested_extras, valid_extras)
return valid_extras
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
factory = self.base._factory
# Add a dependency on the exact base
@ -522,18 +542,7 @@ class ExtrasCandidate(Candidate):
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,
)
valid_extras = self._calculate_valid_requested_extras()
for r in self.base.dist.iter_dependencies(valid_extras):
yield from factory.make_requirements_from_spec(
str(r),
@ -541,7 +550,7 @@ class ExtrasCandidate(Candidate):
valid_extras,
)
def get_install_requirement(self) -> InstallRequirement | None:
def get_install_requirement(self) -> Optional[InstallRequirement]:
# We don't return anything here, because we always
# depend on the base candidate, and we'll get the
# install requirement from that.
@ -552,7 +561,7 @@ class RequiresPythonCandidate(Candidate):
is_installed = False
source_link = None
def __init__(self, py_version_info: tuple[int, ...] | None) -> None:
def __init__(self, py_version_info: Optional[Tuple[int, ...]]) -> None:
if py_version_info is not None:
version_info = normalize_version_info(py_version_info)
else:
@ -566,9 +575,6 @@ class RequiresPythonCandidate(Candidate):
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
@ -578,14 +584,14 @@ class RequiresPythonCandidate(Candidate):
return REQUIRES_PYTHON_IDENTIFIER
@property
def version(self) -> Version:
def version(self) -> CandidateVersion:
return self._version
def format_for_error(self) -> str:
return f"Python {self.version}"
def iter_dependencies(self, with_requires: bool) -> Iterable[Requirement | None]:
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
return ()
def get_install_requirement(self) -> InstallRequirement | None:
def get_install_requirement(self) -> Optional[InstallRequirement]:
return None

View file

@ -1,14 +1,19 @@
from __future__ import annotations
import contextlib
import functools
import logging
from collections.abc import Iterable, Iterator, Mapping, Sequence
from typing import (
TYPE_CHECKING,
Callable,
Dict,
FrozenSet,
Iterable,
Iterator,
List,
Mapping,
NamedTuple,
Protocol,
Optional,
Sequence,
Set,
Tuple,
TypeVar,
cast,
)
@ -16,16 +21,13 @@ from typing import (
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,
)
@ -48,7 +50,7 @@ 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 .base import Candidate, CandidateVersion, Constraint, Requirement
from .candidates import (
AlreadyInstalledCandidate,
BaseCandidate,
@ -68,6 +70,7 @@ from .requirements import (
)
if TYPE_CHECKING:
from typing import Protocol
class ConflictCause(Protocol):
requirement: RequiresPythonRequirement
@ -77,13 +80,13 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
C = TypeVar("C")
Cache = dict[Link, C]
Cache = Dict[Link, C]
class CollectedRootRequirements(NamedTuple):
requirements: list[Requirement]
constraints: dict[str, Constraint]
user_requested: dict[str, int]
requirements: List[Requirement]
constraints: Dict[str, Constraint]
user_requested: Dict[str, int]
class Factory:
@ -92,12 +95,12 @@ class Factory:
finder: PackageFinder,
preparer: RequirementPreparer,
make_install_req: InstallRequirementProvider,
wheel_cache: WheelCache | None,
wheel_cache: Optional[WheelCache],
use_user_site: bool,
force_reinstall: bool,
ignore_installed: bool,
ignore_requires_python: bool,
py_version_info: tuple[int, ...] | None = None,
py_version_info: Optional[Tuple[int, ...]] = None,
) -> None:
self._finder = finder
self.preparer = preparer
@ -111,11 +114,10 @@ class Factory:
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._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()
@ -142,9 +144,9 @@ class Factory:
def _make_extras_candidate(
self,
base: BaseCandidate,
extras: frozenset[str],
extras: FrozenSet[str],
*,
comes_from: InstallRequirement | None = None,
comes_from: Optional[InstallRequirement] = None,
) -> ExtrasCandidate:
cache_key = (id(base), frozenset(canonicalize_name(e) for e in extras))
try:
@ -157,7 +159,7 @@ class Factory:
def _make_candidate_from_dist(
self,
dist: BaseDistribution,
extras: frozenset[str],
extras: FrozenSet[str],
template: InstallRequirement,
) -> Candidate:
try:
@ -172,12 +174,12 @@ class Factory:
def _make_candidate_from_link(
self,
link: Link,
extras: frozenset[str],
extras: FrozenSet[str],
template: InstallRequirement,
name: NormalizedName | None,
version: Version | None,
) -> Candidate | None:
base: BaseCandidate | None = self._make_base_candidate_from_link(
name: Optional[NormalizedName],
version: Optional[CandidateVersion],
) -> Optional[Candidate]:
base: Optional[BaseCandidate] = self._make_base_candidate_from_link(
link, template, name, version
)
if not extras or base is None:
@ -188,9 +190,9 @@ class Factory:
self,
link: Link,
template: InstallRequirement,
name: NormalizedName | None,
version: Version | None,
) -> BaseCandidate | None:
name: Optional[NormalizedName],
version: Optional[CandidateVersion],
) -> Optional[BaseCandidate]:
# TODO: Check already installed candidate, and use it if the link and
# editable flag match.
@ -209,7 +211,7 @@ class Factory:
name=name,
version=version,
)
except (MetadataInconsistent, MetadataInvalid) as e:
except MetadataInconsistent as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
link,
@ -247,7 +249,7 @@ class Factory:
specifier: SpecifierSet,
hashes: Hashes,
prefers_installed: bool,
incompatible_ids: set[int],
incompatible_ids: Set[int],
) -> Iterable[Candidate]:
if not ireqs:
return ()
@ -260,14 +262,14 @@ class Factory:
assert template.req, "Candidates found on index must be PEP 508"
name = canonicalize_name(template.req.name)
extras: frozenset[str] = frozenset()
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:
def _get_installed_candidate() -> Optional[Candidate]:
"""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.
@ -277,15 +279,10 @@ class Factory:
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)
# 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
candidate = self._make_candidate_from_dist(
dist=installed_dist,
extras=extras,
@ -302,7 +299,7 @@ class Factory:
specifier=specifier,
hashes=hashes,
)
icans = result.applicable_candidates
icans = list(result.iter_applicable())
# PEP 592: Yanked releases are ignored unless the specifier
# explicitly pins a version (via '==' or '===') that can be
@ -346,7 +343,7 @@ class Factory:
def _iter_explicit_candidates_from_base(
self,
base_requirements: Iterable[Requirement],
extras: frozenset[str],
extras: FrozenSet[str],
) -> Iterator[Candidate]:
"""Produce explicit candidates from the base given an extra-ed package.
@ -394,11 +391,10 @@ class Factory:
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] = []
explicit_candidates: Set[Candidate] = set()
ireqs: List[InstallRequirement] = []
for req in requirements[identifier]:
cand, ireq = req.get_candidate_lookup()
if cand is not None:
@ -460,7 +456,7 @@ class Factory:
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])
and all(req.is_satisfied_by(c) for req in requirements[identifier])
)
def _make_requirements_from_install_req(
@ -517,7 +513,7 @@ class Factory:
)
def collect_root_requirements(
self, root_ireqs: list[InstallRequirement]
self, root_ireqs: List[InstallRequirement]
) -> CollectedRootRequirements:
collected = CollectedRootRequirements([], {}, {})
for i, ireq in enumerate(root_ireqs):
@ -566,7 +562,7 @@ class Factory:
def make_requirements_from_spec(
self,
specifier: str,
comes_from: InstallRequirement | None,
comes_from: Optional[InstallRequirement],
requested_extras: Iterable[str] = (),
) -> Iterator[Requirement]:
"""
@ -584,7 +580,7 @@ class Factory:
def make_requires_python_requirement(
self,
specifier: SpecifierSet,
) -> Requirement | None:
) -> Optional[Requirement]:
if self._ignore_requires_python:
return None
# Don't bother creating a dependency for an empty Requires-Python.
@ -592,7 +588,9 @@ class Factory:
return None
return RequiresPythonRequirement(specifier, self._python_candidate)
def get_wheel_cache_entry(self, link: Link, name: str | None) -> CacheEntry | None:
def get_wheel_cache_entry(
self, link: Link, name: Optional[str]
) -> Optional[CacheEntry]:
"""Look up the link in the wheel cache.
If ``preparer.require_hashes`` is True, don't use the wheel cache,
@ -606,10 +604,10 @@ class Factory:
return self._wheel_cache.get_cache_entry(
link=link,
package_name=name,
supported_tags=self._supported_tags_cache,
supported_tags=get_supported(),
)
def get_dist_to_uninstall(self, candidate: Candidate) -> BaseDistribution | None:
def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistribution]:
# 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.
@ -638,7 +636,7 @@ class Factory:
return None
def _report_requires_python_error(
self, causes: Sequence[ConflictCause]
self, causes: Sequence["ConflictCause"]
) -> UnsupportedPythonVersion:
assert causes, "Requires-Python error reported with no cause"
@ -660,7 +658,7 @@ class Factory:
return UnsupportedPythonVersion(message)
def _report_single_requirement_conflict(
self, req: Requirement, parent: Candidate | None
self, req: Requirement, parent: Optional[Candidate]
) -> DistributionNotFound:
if parent is None:
req_disp = str(req)
@ -670,8 +668,8 @@ class Factory:
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()
versions_set: Set[CandidateVersion] = set()
yanked_versions_set: Set[CandidateVersion] = set()
for c in cands:
is_yanked = c.link.is_yanked if c.link else False
if is_yanked:
@ -711,25 +709,10 @@ class Factory:
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],
e: "ResolutionImpossible[Requirement, Candidate]",
constraints: Dict[str, Constraint],
) -> InstallationError:
assert e.causes, "Installation error reported with no cause"
@ -754,7 +737,7 @@ class Factory:
# 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))
req, parent = e.causes[0]
if req.name not in constraints:
return self._report_single_requirement_conflict(req, parent)
@ -762,7 +745,7 @@ class Factory:
# satisfied at once.
# A couple of formatting helpers
def text_join(parts: list[str]) -> str:
def text_join(parts: List[str]) -> str:
if len(parts) == 1:
return parts[0]
@ -811,28 +794,12 @@ class Factory:
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 "
+ "2. remove package versions to allow pip attempt to solve "
+ "the dependency conflict\n"
)

View file

@ -8,21 +8,30 @@ 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
import functools
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Set, Tuple
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]]]
IndexCandidateInfo = tuple[_BaseVersion, Callable[[], Optional[Candidate]]]
if TYPE_CHECKING:
SequenceCandidate = Sequence[Candidate]
else:
# For compatibility: Python before 3.9 does not support using [] on the
# Sequence class.
#
# >>> from collections.abc import Sequence
# >>> Sequence[str]
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: 'ABCMeta' object is not subscriptable
#
# TODO: Remove this block after dropping Python 3.8 support.
SequenceCandidate = Sequence
def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
@ -31,29 +40,15 @@ def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
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()
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)
candidate = func()
if candidate is None:
continue
yield candidate
versions_found.add(version)
def _iter_built_with_prepended(
@ -67,7 +62,7 @@ def _iter_built_with_prepended(
normal ordering, except skipped when the version is already installed.
"""
yield installed
versions_found: set[_BaseVersion] = {installed.version}
versions_found: Set[_BaseVersion] = {installed.version}
for version, func in infos:
if version in versions_found:
continue
@ -91,7 +86,7 @@ def _iter_built_with_inserted(
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()
versions_found: Set[_BaseVersion] = set()
for version, func in infos:
if version in versions_found:
continue
@ -110,7 +105,7 @@ def _iter_built_with_inserted(
yield installed
class FoundCandidates(Sequence[Candidate]):
class FoundCandidates(SequenceCandidate):
"""A lazy sequence to provide candidates to the resolver.
The intended usage is to return this from `find_matches()` so the resolver
@ -122,15 +117,14 @@ class FoundCandidates(Sequence[Candidate]):
def __init__(
self,
get_infos: Callable[[], Iterator[IndexCandidateInfo]],
installed: Candidate | None,
installed: Optional[Candidate],
prefers_installed: bool,
incompatible_ids: set[int],
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
@ -154,13 +148,8 @@ class FoundCandidates(Sequence[Candidate]):
# performance reasons).
raise NotImplementedError("don't do this")
@functools.lru_cache(maxsize=1)
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
return any(self)

View file

@ -1,21 +1,21 @@
from __future__ import annotations
import collections
import math
from collections.abc import Iterable, Iterator, Mapping, Sequence
from functools import cache
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
Iterator,
Mapping,
Sequence,
TypeVar,
Union,
)
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
@ -54,7 +54,7 @@ def _get_with_identifier(
mapping: Mapping[str, V],
identifier: str,
default: D,
) -> D | V:
) -> Union[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
@ -89,80 +89,29 @@ class PipProvider(_ProviderBase):
def __init__(
self,
factory: Factory,
constraints: dict[str, Constraint],
constraints: Dict[str, Constraint],
ignore_dependencies: bool,
upgrade_strategy: str,
user_requested: dict[str, int],
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._known_depths: Dict[str, float] = collections.defaultdict(lambda: math.inf)
@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:
def identify(self, requirement_or_candidate: Union[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:
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
@ -170,20 +119,18 @@ class PipProvider(_ProviderBase):
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).
* Prefer if any of the known requirements is "direct", e.g. points to an
explicit URL.
* If equal, prefer if any requirement is "pinned", i.e. contains
operator ``===`` or ``==``.
* If equal, calculate an approximate "depth" and resolve requirements
closer to the user-specified requirements first. If the depth cannot
by determined (eg: due to no matching parents), it is considered
infinite.
* Order user-specified requirements by the order they are specified.
* If equal, prefers "non-free" requirements, i.e. contains at least one
operator, such as ``>=`` or ``<``.
* If equal, order alphabetically for consistency (helps debuggability).
"""
try:
next(iter(information[identifier]))
@ -194,39 +141,55 @@ class PipProvider(_ProviderBase):
else:
has_information = True
if not has_information:
direct = False
ireqs: tuple[InstallRequirement | None, ...] = ()
if has_information:
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
candidate, ireqs = zip(*lookups)
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)
candidate, ireqs = None, ()
operators: list[tuple[str, str]] = [
(specifier.operator, specifier.version)
operators = [
specifier.operator
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
)
direct = candidate is not None
pinned = any(op[:2] == "==" for op in operators)
unfree = bool(operators)
try:
requested_order: Union[int, float] = self._user_requested[identifier]
except KeyError:
requested_order = math.inf
if has_information:
parent_depths = (
self._known_depths[parent.name] if parent is not None else 0.0
for _, parent in information[identifier]
)
inferred_depth = min(d for d in parent_depths) + 1.0
else:
inferred_depth = math.inf
else:
inferred_depth = 1.0
self._known_depths[identifier] = inferred_depth
requested_order = self._user_requested.get(identifier, math.inf)
# Requires-Python has only one candidate and the check is basically
# free, so we always do it first to avoid needless work if it fails.
requires_python = identifier == REQUIRES_PYTHON_IDENTIFIER
# Prefer the causes of backtracking on the assumption that the problem
# resolving the dependency tree is related to the failures that caused
# the backtracking
backtrack_cause = self.is_backtrack_cause(identifier, backtrack_causes)
return (
not requires_python,
not direct,
not pinned,
not upper_bounded,
not backtrack_cause,
inferred_depth,
requested_order,
not unfree,
identifier,
@ -271,15 +234,22 @@ class PipProvider(_ProviderBase):
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:
def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool:
return requirement.is_satisfied_by(candidate)
def get_dependencies(self, candidate: Candidate) -> Iterable[Requirement]:
def get_dependencies(self, candidate: Candidate) -> Sequence[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)
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]
@staticmethod
def is_backtrack_cause(
identifier: str, backtrack_causes: Sequence["PreferenceInformation"]
) -> bool:
for backtrack_cause in backtrack_causes:
if identifier == backtrack_cause.requirement.name:
return True
if backtrack_cause.parent and identifier == backtrack_cause.parent.name:
return True
return False

View file

@ -1,21 +1,17 @@
from __future__ import annotations
from collections import defaultdict
from collections.abc import Mapping
from logging import getLogger
from typing import Any
from typing import Any, DefaultDict
from pip._vendor.resolvelib.reporters import BaseReporter
from .base import Candidate, Constraint, Requirement
from .base import Candidate, 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 {}
class PipReporter(BaseReporter):
def __init__(self) -> None:
self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int)
self._messages_at_reject_count = {
1: (
@ -37,40 +33,29 @@ class PipReporter(BaseReporter[Requirement, Candidate, str]):
}
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))
if count not in self._messages_at_reject_count:
return
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
# Inspired by Factory.get_installation_error
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]):
class PipDebuggingReporter(BaseReporter):
"""A reporter that does an info log for every event it sees."""
def starting(self) -> None:
@ -81,14 +66,11 @@ class PipDebuggingReporter(BaseReporter[Requirement, Candidate, str]):
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:
def adding_requirement(self, requirement: Requirement, parent: Candidate) -> None:
logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent)
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:

View file

@ -1,7 +1,3 @@
from __future__ import annotations
from typing import Any
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
@ -21,14 +17,6 @@ class ExplicitRequirement(Requirement):
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
@ -53,36 +41,14 @@ 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"
@ -130,38 +96,14 @@ class SpecifierWithoutExtrasRequirement(SpecifierRequirement):
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
def __str__(self) -> str:
@ -170,21 +112,6 @@ class RequiresPythonRequirement(Requirement):
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
@ -221,14 +148,6 @@ class UnsatisfiableRequirement(Requirement):
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

View file

@ -1,18 +1,15 @@
from __future__ import annotations
import contextlib
import functools
import logging
import os
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible, ResolutionTooDeep
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
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
@ -45,7 +42,7 @@ class Resolver(BaseResolver):
self,
preparer: RequirementPreparer,
finder: PackageFinder,
wheel_cache: WheelCache | None,
wheel_cache: Optional[WheelCache],
make_install_req: InstallRequirementProvider,
use_user_site: bool,
ignore_dependencies: bool,
@ -53,7 +50,7 @@ class Resolver(BaseResolver):
ignore_requires_python: bool,
force_reinstall: bool,
upgrade_strategy: str,
py_version_info: tuple[int, ...] | None = None,
py_version_info: Optional[Tuple[int, ...]] = None,
):
super().__init__()
assert upgrade_strategy in self._allowed_strategies
@ -71,10 +68,10 @@ class Resolver(BaseResolver):
)
self.ignore_dependencies = ignore_dependencies
self.upgrade_strategy = upgrade_strategy
self._result: Result | None = None
self._result: Optional[Result] = None
def resolve(
self, root_reqs: list[InstallRequirement], check_supported_wheels: bool
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
) -> RequirementSet:
collected = self.factory.collect_root_requirements(root_reqs)
provider = PipProvider(
@ -85,10 +82,9 @@ class Resolver(BaseResolver):
user_requested=collected.user_requested,
)
if "PIP_RESOLVER_DEBUG" in os.environ:
reporter: BaseReporter[Requirement, Candidate, str] = PipDebuggingReporter()
reporter: BaseReporter = PipDebuggingReporter()
else:
reporter = PipReporter(constraints=provider.constraints)
reporter = PipReporter()
resolver: RLResolver[Requirement, Candidate, str] = RLResolver(
provider,
reporter,
@ -106,8 +102,6 @@ class Resolver(BaseResolver):
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
@ -181,11 +175,16 @@ class Resolver(BaseResolver):
req_set.add_named_requirement(ireq)
reqs = req_set.all_requirements
self.factory.preparer.prepare_linked_requirements_more(reqs)
for req in reqs:
req.prepared = True
req.needs_more_preparation = False
return req_set
def get_installation_order(
self, req_set: RequirementSet
) -> list[InstallRequirement]:
) -> List[InstallRequirement]:
"""Get order for installation of requirements in RequirementSet.
The returned list contains a requirement before another that depends on
@ -216,8 +215,8 @@ class Resolver(BaseResolver):
def get_topological_weights(
graph: DirectedGraph[str | None], requirement_keys: set[str]
) -> dict[str | None, int]:
graph: "DirectedGraph[Optional[str]]", requirement_keys: Set[str]
) -> Dict[Optional[str], int]:
"""Assign weights to each node based on how "deep" they are.
This implementation may change at any point in the future without prior
@ -243,25 +242,14 @@ def get_topological_weights(
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]] = {}
path: Set[Optional[str]] = set()
weights: Dict[Optional[str], int] = {}
def visit(node: str | None) -> None:
def visit(node: Optional[str]) -> 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):
@ -271,14 +259,14 @@ def get_topological_weights(
if node not in requirement_keys:
return
cur_weights.append(len(path))
weights[node] = cur_weights
last_known_parent_count = weights.get(node, 0)
weights[node] = max(last_known_parent_count, len(path))
# 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.
# Simplify the graph, pruning leaves that have no dependencies.
# This is needed for large graphs (say over 200 packages) because the
# `visit` function is exponentially slower then, taking minutes.
# See https://github.com/pypa/pip/issues/10557
# We repeat the pruning step until we have no more leaves to remove.
# We will loop until we explicitly break the loop.
while True:
leaves = set()
for key in graph:
@ -298,13 +286,12 @@ def get_topological_weights(
for leaf in leaves:
if leaf not in requirement_keys:
continue
weights[leaf] = [weight]
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.
# Visit the remaining graph.
# `None` is guaranteed to be the root node by resolvelib.
visit(None)
@ -313,15 +300,13 @@ def get_topological_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()}
return weights
def _req_set_item_sorter(
item: tuple[str, InstallRequirement],
weights: dict[str | None, int],
) -> tuple[int, str]:
item: Tuple[str, InstallRequirement],
weights: Dict[Optional[str], int],
) -> Tuple[int, str]:
"""Key function used to sort install requirements for installation.
Based on the "weight" mapping calculated in ``get_installation_order()``.