This commit is contained in:
ton
2024-10-07 10:13:40 +07:00
parent aa1631742f
commit 3a7d696db6
9729 changed files with 1832837 additions and 161742 deletions

View File

@@ -10,9 +10,6 @@ for sub-dependencies
a. "first found, wins" (where the order is breadth first)
"""
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import logging
import sys
from collections import defaultdict
@@ -52,7 +49,7 @@ from pip._internal.utils.packaging import check_requires_python
logger = logging.getLogger(__name__)
DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
DiscoveredDependencies = DefaultDict[Optional[str], List[InstallRequirement]]
def _check_dist_requires_python(
@@ -104,9 +101,8 @@ def _check_dist_requires_python(
return
raise UnsupportedPythonVersion(
"Package {!r} requires a different Python: {} not in {!r}".format(
dist.raw_name, version, requires_python
)
f"Package {dist.raw_name!r} requires a different Python: "
f"{version} not in {requires_python!r}"
)
@@ -231,9 +227,7 @@ class Resolver(BaseResolver):
tags = compatibility_tags.get_supported()
if requirement_set.check_supported_wheels and not wheel.supported(tags):
raise InstallationError(
"{} is not a supported wheel on this platform.".format(
wheel.filename
)
f"{wheel.filename} is not a supported wheel on this platform."
)
# This next bit is really a sanity check.
@@ -248,9 +242,9 @@ class Resolver(BaseResolver):
return [install_req], None
try:
existing_req: Optional[
InstallRequirement
] = requirement_set.get_requirement(install_req.name)
existing_req: Optional[InstallRequirement] = (
requirement_set.get_requirement(install_req.name)
)
except KeyError:
existing_req = None
@@ -265,9 +259,8 @@ class Resolver(BaseResolver):
)
if has_conflicting_requirement:
raise InstallationError(
"Double requirement given: {} (already in {}, name={!r})".format(
install_req, existing_req, install_req.name
)
f"Double requirement given: {install_req} "
f"(already in {existing_req}, name={install_req.name!r})"
)
# When no existing requirement exists, add the requirement as a
@@ -287,9 +280,9 @@ class Resolver(BaseResolver):
)
if does_not_satisfy_constraint:
raise InstallationError(
"Could not satisfy constraints for '{}': "
f"Could not satisfy constraints for '{install_req.name}': "
"installation from path or url cannot be "
"constrained to a version".format(install_req.name)
"constrained to a version"
)
# If we're now installing a constraint, mark the existing
# object for real installation.
@@ -325,6 +318,7 @@ 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
@@ -398,9 +392,9 @@ class Resolver(BaseResolver):
# "UnicodeEncodeError: 'ascii' codec can't encode character"
# in Python 2 when the reason contains non-ascii characters.
"The candidate selected for download or install is a "
"yanked version: {candidate}\n"
"Reason for being yanked: {reason}"
).format(candidate=best_candidate, reason=reason)
f"yanked version: {best_candidate}\n"
f"Reason for being yanked: {reason}"
)
logger.warning(msg)
return link
@@ -423,6 +417,8 @@ 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,
@@ -536,6 +532,7 @@ 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

View File

@@ -1,31 +1,29 @@
from typing import FrozenSet, Iterable, Optional, Tuple, Union
from dataclasses import dataclass
from typing import FrozenSet, Iterable, Optional, Tuple
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import LegacyVersion, Version
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]]
CandidateVersion = Union[LegacyVersion, Version]
def format_name(project: str, extras: FrozenSet[str]) -> str:
def format_name(project: NormalizedName, extras: FrozenSet[NormalizedName]) -> str:
if not extras:
return project
canonical_extras = sorted(canonicalize_name(e) for e in extras)
return "{}[{}]".format(project, ",".join(canonical_extras))
extras_expr = ",".join(sorted(extras))
return f"{project}[{extras_expr}]"
@dataclass(frozen=True)
class Constraint:
def __init__(
self, specifier: SpecifierSet, hashes: Hashes, links: FrozenSet[Link]
) -> None:
self.specifier = specifier
self.hashes = hashes
self.links = links
specifier: SpecifierSet
hashes: Hashes
links: FrozenSet[Link]
@classmethod
def empty(cls) -> "Constraint":
@@ -116,7 +114,7 @@ class Candidate:
raise NotImplementedError("Override in subclass")
@property
def version(self) -> CandidateVersion:
def version(self) -> Version:
raise NotImplementedError("Override in subclass")
@property

View File

@@ -2,6 +2,7 @@ import logging
import sys
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
@@ -9,6 +10,7 @@ from pip._internal.exceptions import (
HashError,
InstallationSubprocessError,
MetadataInconsistent,
MetadataInvalid,
)
from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link, links_equivalent
@@ -21,7 +23,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, CandidateVersion, Requirement, format_name
from .base import Candidate, Requirement, format_name
if TYPE_CHECKING:
from .factory import Factory
@@ -145,7 +147,7 @@ class _InstallRequirementBackedCandidate(Candidate):
ireq: InstallRequirement,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
version: Optional[Version] = None,
) -> None:
self._link = link
self._source_link = source_link
@@ -154,18 +156,20 @@ class _InstallRequirementBackedCandidate(Candidate):
self._name = name
self._version = version
self.dist = self._prepare()
self._hash: Optional[int] = None
def __str__(self) -> str:
return f"{self.name} {self.version}"
def __repr__(self) -> str:
return "{class_name}({link!r})".format(
class_name=self.__class__.__name__,
link=str(self._link),
)
return f"{self.__class__.__name__}({str(self._link)!r})"
def __hash__(self) -> int:
return hash((self.__class__, self._link))
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__):
@@ -188,16 +192,15 @@ class _InstallRequirementBackedCandidate(Candidate):
return self.project_name
@property
def version(self) -> CandidateVersion:
def version(self) -> Version:
if self._version is None:
self._version = self.dist.version
return self._version
def format_for_error(self) -> str:
return "{} {} (from {})".format(
self.name,
self.version,
self._link.file_path if self._link.is_file else self._link,
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:
@@ -219,6 +222,13 @@ 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:
@@ -240,7 +250,7 @@ class _InstallRequirementBackedCandidate(Candidate):
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
requires = self.dist.iter_dependencies() if with_requires else ()
for r in requires:
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
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) -> Optional[InstallRequirement]:
@@ -256,7 +266,7 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
template: InstallRequirement,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
version: Optional[Version] = None,
) -> None:
source_link = link
cache_entry = factory.get_wheel_cache_entry(source_link, name)
@@ -272,9 +282,9 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
# Version may not be present for PEP 508 direct URLs
if version is not None:
wheel_version = Version(wheel.version)
assert version == wheel_version, "{!r} != {!r} for wheel {}".format(
version, wheel_version, name
)
assert (
version == wheel_version
), f"{version!r} != {wheel_version!r} for wheel {name}"
if cache_entry is not None:
assert ireq.link.is_wheel
@@ -313,7 +323,7 @@ class EditableCandidate(_InstallRequirementBackedCandidate):
template: InstallRequirement,
factory: "Factory",
name: Optional[NormalizedName] = None,
version: Optional[CandidateVersion] = None,
version: Optional[Version] = None,
) -> None:
super().__init__(
link=link,
@@ -354,18 +364,15 @@ class AlreadyInstalledCandidate(Candidate):
return str(self.dist)
def __repr__(self) -> str:
return "{class_name}({distribution!r})".format(
class_name=self.__class__.__name__,
distribution=self.dist,
)
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.__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
return hash((self.name, self.version))
@property
def project_name(self) -> NormalizedName:
@@ -376,7 +383,7 @@ class AlreadyInstalledCandidate(Candidate):
return self.project_name
@property
def version(self) -> CandidateVersion:
def version(self) -> Version:
if self._version is None:
self._version = self.dist.version
return self._version
@@ -392,7 +399,7 @@ class AlreadyInstalledCandidate(Candidate):
if not with_requires:
return
for r in self.dist.iter_dependencies():
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
def get_install_requirement(self) -> Optional[InstallRequirement]:
return None
@@ -427,20 +434,27 @@ class ExtrasCandidate(Candidate):
self,
base: BaseCandidate,
extras: FrozenSet[str],
*,
comes_from: Optional[InstallRequirement] = 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 = extras
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 "{class_name}(base={base!r}, extras={extras!r})".format(
class_name=self.__class__.__name__,
base=self.base,
extras=self.extras,
)
return f"{self.__class__.__name__}(base={self.base!r}, extras={self.extras!r})"
def __hash__(self) -> int:
return hash((self.base, self.extras))
@@ -460,7 +474,7 @@ class ExtrasCandidate(Candidate):
return format_name(self.base.project_name, self.extras)
@property
def version(self) -> CandidateVersion:
def version(self) -> Version:
return self.base.version
def format_for_error(self) -> str:
@@ -502,11 +516,11 @@ class ExtrasCandidate(Candidate):
)
for r in self.base.dist.iter_dependencies(valid_extras):
requirement = factory.make_requirement_from_spec(
str(r), self.base._ireq, valid_extras
yield from factory.make_requirements_from_spec(
str(r),
self._comes_from,
valid_extras,
)
if requirement:
yield requirement
def get_install_requirement(self) -> Optional[InstallRequirement]:
# We don't return anything here, because we always
@@ -542,7 +556,7 @@ class RequiresPythonCandidate(Candidate):
return REQUIRES_PYTHON_IDENTIFIER
@property
def version(self) -> CandidateVersion:
def version(self) -> Version:
return self._version
def format_for_error(self) -> str:

View File

@@ -3,6 +3,7 @@ import functools
import logging
from typing import (
TYPE_CHECKING,
Callable,
Dict,
FrozenSet,
Iterable,
@@ -11,6 +12,7 @@ from typing import (
Mapping,
NamedTuple,
Optional,
Protocol,
Sequence,
Set,
Tuple,
@@ -21,6 +23,7 @@ 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 Version
from pip._vendor.resolvelib import ResolutionImpossible
from pip._internal.cache import CacheEntry, WheelCache
@@ -28,6 +31,7 @@ from pip._internal.exceptions import (
DistributionNotFound,
InstallationError,
MetadataInconsistent,
MetadataInvalid,
UnsupportedPythonVersion,
UnsupportedWheel,
)
@@ -36,7 +40,10 @@ 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_from_link_and_ireq
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,
@@ -47,7 +54,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, CandidateVersion, Constraint, Requirement
from .base import Candidate, Constraint, Requirement
from .candidates import (
AlreadyInstalledCandidate,
BaseCandidate,
@@ -62,11 +69,11 @@ from .requirements import (
ExplicitRequirement,
RequiresPythonRequirement,
SpecifierRequirement,
SpecifierWithoutExtrasRequirement,
UnsatisfiableRequirement,
)
if TYPE_CHECKING:
from typing import Protocol
class ConflictCause(Protocol):
requirement: RequiresPythonRequirement
@@ -112,8 +119,9 @@ class Factory:
self._editable_candidate_cache: Cache[EditableCandidate] = {}
self._installed_candidate_cache: Dict[str, AlreadyInstalledCandidate] = {}
self._extras_candidate_cache: Dict[
Tuple[int, FrozenSet[str]], ExtrasCandidate
Tuple[int, FrozenSet[NormalizedName]], ExtrasCandidate
] = {}
self._supported_tags_cache = get_supported()
if not ignore_installed:
env = get_default_environment()
@@ -132,19 +140,23 @@ class Factory:
if not link.is_wheel:
return
wheel = Wheel(link.filename)
if wheel.supported(self._finder.target_python.get_tags()):
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]
self,
base: BaseCandidate,
extras: FrozenSet[str],
*,
comes_from: Optional[InstallRequirement] = None,
) -> ExtrasCandidate:
cache_key = (id(base), extras)
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)
candidate = ExtrasCandidate(base, extras, comes_from=comes_from)
self._extras_candidate_cache[cache_key] = candidate
return candidate
@@ -161,7 +173,7 @@ class Factory:
self._installed_candidate_cache[dist.canonical_name] = base
if not extras:
return base
return self._make_extras_candidate(base, extras)
return self._make_extras_candidate(base, extras, comes_from=template)
def _make_candidate_from_link(
self,
@@ -169,8 +181,22 @@ class Factory:
extras: FrozenSet[str],
template: InstallRequirement,
name: Optional[NormalizedName],
version: Optional[CandidateVersion],
version: Optional[Version],
) -> Optional[Candidate]:
base: Optional[BaseCandidate] = 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: Optional[NormalizedName],
version: Optional[Version],
) -> Optional[BaseCandidate]:
# TODO: Check already installed candidate, and use it if the link and
# editable flag match.
@@ -189,7 +215,7 @@ class Factory:
name=name,
version=version,
)
except MetadataInconsistent as e:
except (MetadataInconsistent, MetadataInvalid) as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
link,
@@ -199,7 +225,7 @@ class Factory:
self._build_failures[link] = e
return None
base: BaseCandidate = self._editable_candidate_cache[link]
return self._editable_candidate_cache[link]
else:
if link not in self._link_candidate_cache:
try:
@@ -219,11 +245,7 @@ class Factory:
)
self._build_failures[link] = e
return None
base = self._link_candidate_cache[link]
if not extras:
return base
return self._make_extras_candidate(base, extras)
return self._link_candidate_cache[link]
def _iter_found_candidates(
self,
@@ -357,9 +379,8 @@ class Factory:
"""
for link in constraint.links:
self._fail_if_link_is_unsupported_wheel(link)
candidate = self._make_candidate_from_link(
candidate = self._make_base_candidate_from_link(
link,
extras=frozenset(),
template=install_req_from_link_and_ireq(link, template),
name=canonicalize_name(identifier),
version=None,
@@ -374,6 +395,7 @@ 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()
@@ -385,16 +407,21 @@ class Factory:
if ireq is not None:
ireqs.append(ireq)
# If the current identifier contains extras, add explicit candidates
# from entries from extra-less identifier.
# 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)
explicit_candidates.update(
self._iter_explicit_candidates_from_base(
requirements.get(parsed_requirement.name, ()),
frozenset(parsed_requirement.extras),
),
)
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
@@ -434,40 +461,61 @@ class Factory:
for c in explicit_candidates
if id(c) not in incompat_ids
and constraint.is_satisfied_by(c)
and all(req.is_satisfied_by(c) for req in requirements[identifier])
and all(is_satisfied_by(req, c) for req in requirements[identifier])
)
def _make_requirement_from_install_req(
def _make_requirements_from_install_req(
self, ireq: InstallRequirement, requested_extras: Iterable[str]
) -> Optional[Requirement]:
) -> 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,
)
return None
if not ireq.link:
return SpecifierRequirement(ireq)
self._fail_if_link_is_unsupported_wheel(ireq.link)
cand = self._make_candidate_from_link(
ireq.link,
extras=frozenset(ireq.extras),
template=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]
return UnsatisfiableRequirement(canonicalize_name(ireq.name))
return self.make_requirement_from_candidate(cand)
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]
@@ -488,15 +536,27 @@ class Factory:
else:
collected.constraints[name] = Constraint.from_ireq(ireq)
else:
req = self._make_requirement_from_install_req(
ireq,
requested_extras=(),
reqs = list(
self._make_requirements_from_install_req(
ireq,
requested_extras=(),
)
)
if req is None:
if not reqs:
continue
if ireq.user_supplied and req.name not in collected.user_requested:
collected.user_requested[req.name] = i
collected.requirements.append(req)
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(
@@ -504,14 +564,23 @@ class Factory:
) -> ExplicitRequirement:
return ExplicitRequirement(candidate)
def make_requirement_from_spec(
def make_requirements_from_spec(
self,
specifier: str,
comes_from: Optional[InstallRequirement],
requested_extras: Iterable[str] = (),
) -> Optional[Requirement]:
) -> 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_requirement_from_install_req(ireq, requested_extras)
return self._make_requirements_from_install_req(ireq, requested_extras)
def make_requires_python_requirement(
self,
@@ -540,7 +609,7 @@ class Factory:
return self._wheel_cache.get_cache_entry(
link=link,
package_name=name,
supported_tags=get_supported(),
supported_tags=self._supported_tags_cache,
)
def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistribution]:
@@ -603,8 +672,26 @@ class Factory:
cands = self._finder.find_all_candidates(req.project_name)
skipped_by_requires_python = self._finder.requires_python_skipped_reasons()
versions = [str(v) for v in sorted({c.version for c in cands})]
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 "
@@ -692,8 +779,8 @@ class Factory:
info = "the requested packages"
msg = (
"Cannot install {} because these package versions "
"have conflicting dependencies.".format(info)
f"Cannot install {info} because these package versions "
"have conflicting dependencies."
)
logger.critical(msg)
msg = "\nThe conflict is caused by:"
@@ -717,7 +804,7 @@ class Factory:
+ "\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 attempt to solve "
+ "2. remove package versions to allow pip to attempt to solve "
+ "the dependency conflict\n"
)

View File

@@ -9,13 +9,18 @@ something.
"""
import functools
import logging
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]]]
if TYPE_CHECKING:
@@ -44,11 +49,25 @@ def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
for version, func in infos:
if version in versions_found:
continue
candidate = func()
if candidate is None:
continue
yield candidate
versions_found.add(version)
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(

View File

@@ -1,5 +1,6 @@
import collections
import math
from functools import lru_cache
from typing import (
TYPE_CHECKING,
Dict,
@@ -234,8 +235,10 @@ class PipProvider(_ProviderBase):
constraint=constraint,
prefers_installed=(not _eligible_for_upgrade(identifier)),
incompatibilities=incompatibilities,
is_satisfied_by=self.is_satisfied_by,
)
@lru_cache(maxsize=None)
def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool:
return requirement.is_satisfied_by(candidate)

View File

@@ -66,6 +66,7 @@ class PipDebuggingReporter(BaseReporter):
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)

View File

@@ -1,6 +1,9 @@
from typing import Any, Optional
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._internal.req.constructors import install_req_drop_extras
from pip._internal.req.req_install import InstallRequirement
from .base import Candidate, CandidateLookup, Requirement, format_name
@@ -14,10 +17,15 @@ class ExplicitRequirement(Requirement):
return str(self.candidate)
def __repr__(self) -> str:
return "{class_name}({candidate!r})".format(
class_name=self.__class__.__name__,
candidate=self.candidate,
)
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:
@@ -43,16 +51,35 @@ 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._extras = frozenset(ireq.extras)
self._equal_cache: Optional[str] = None
self._hash: Optional[int] = 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 "{class_name}({requirement!r})".format(
class_name=self.__class__.__name__,
requirement=str(self._ireq.req),
)
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:
@@ -92,20 +119,68 @@ class SpecifierRequirement(Requirement):
return spec.contains(candidate.version, prereleases=True)
class SpecifierWithoutExtrasRequirement(SpecifierRequirement):
"""
Requirement backed by an install requirement on a base package.
Trims extras from its install requirement if there are any.
"""
def __init__(self, ireq: InstallRequirement) -> None:
assert ireq.link is None, "This is a link, not a specifier"
self._ireq = install_req_drop_extras(ireq)
self._equal_cache: Optional[str] = None
self._hash: Optional[int] = 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: Optional[int] = None
self._candidate = match
def __str__(self) -> str:
return f"Python {self.specifier}"
def __repr__(self) -> str:
return "{class_name}({specifier!r})".format(
class_name=self.__class__.__name__,
specifier=str(self.specifier),
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
@@ -142,10 +217,15 @@ class UnsatisfiableRequirement(Requirement):
return f"{self._name} (unavailable)"
def __repr__(self) -> str:
return "{class_name}({name!r})".format(
class_name=self.__class__.__name__,
name=str(self._name),
)
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:

View File

@@ -1,3 +1,4 @@
import contextlib
import functools
import logging
import os
@@ -11,6 +12,7 @@ from pip._vendor.resolvelib.structs import DirectedGraph
from pip._internal.cache import WheelCache
from pip._internal.index.package_finder import PackageFinder
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import install_req_extend_extras
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_set import RequirementSet
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
@@ -19,6 +21,7 @@ from pip._internal.resolution.resolvelib.reporter import (
PipDebuggingReporter,
PipReporter,
)
from pip._internal.utils.packaging import get_requirement
from .base import Candidate, Requirement
from .factory import Factory
@@ -101,9 +104,24 @@ class Resolver(BaseResolver):
raise error from e
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
for candidate in result.mapping.values():
# process candidates with extras last to ensure their base equivalent is
# already in the req_set if appropriate.
# Python's sort is stable so using a binary key function keeps relative order
# within both subsets.
for candidate in sorted(
result.mapping.values(), key=lambda c: c.name != c.project_name
):
ireq = candidate.get_install_requirement()
if ireq is None:
if candidate.name != candidate.project_name:
# extend existing req's extras
with contextlib.suppress(KeyError):
req = req_set.get_requirement(candidate.project_name)
req_set.add_named_requirement(
install_req_extend_extras(
req, get_requirement(candidate.name).extras
)
)
continue
# Check if there is already an installation under the same name,