mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
21857784d6
All extras from user input or dependant package metadata are properly normalized for comparison and resolution. This ensures requests for extras from a dependant can always correctly find the normalized extra in the dependency, even if the requested extra name is not normalized. Note that this still relies on the declaration of extra names in the dependency's package metadata to be properly normalized when the package is built, since correct comparison between an extra name's normalized and non-normalized forms requires change to the metadata parsing logic, which is only available in packaging 22.0 and up, which pip does not use at the moment.
166 lines
5.4 KiB
Python
166 lines
5.4 KiB
Python
from pip._vendor.packaging.specifiers import SpecifierSet
|
|
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
|
|
|
from pip._internal.req.req_install import InstallRequirement
|
|
|
|
from .base import Candidate, CandidateLookup, Requirement, format_name
|
|
|
|
|
|
class ExplicitRequirement(Requirement):
|
|
def __init__(self, candidate: Candidate) -> None:
|
|
self.candidate = candidate
|
|
|
|
def __str__(self) -> str:
|
|
return str(self.candidate)
|
|
|
|
def __repr__(self) -> str:
|
|
return "{class_name}({candidate!r})".format(
|
|
class_name=self.__class__.__name__,
|
|
candidate=self.candidate,
|
|
)
|
|
|
|
@property
|
|
def project_name(self) -> NormalizedName:
|
|
# No need to canonicalize - the candidate did this
|
|
return self.candidate.project_name
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
# No need to canonicalize - the candidate did this
|
|
return self.candidate.name
|
|
|
|
def format_for_error(self) -> str:
|
|
return self.candidate.format_for_error()
|
|
|
|
def get_candidate_lookup(self) -> CandidateLookup:
|
|
return self.candidate, None
|
|
|
|
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
|
return candidate == self.candidate
|
|
|
|
|
|
class SpecifierRequirement(Requirement):
|
|
def __init__(self, ireq: InstallRequirement) -> None:
|
|
assert ireq.link is None, "This is a link, not a specifier"
|
|
self._ireq = ireq
|
|
self._extras = frozenset(canonicalize_name(e) for e in ireq.extras)
|
|
|
|
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),
|
|
)
|
|
|
|
@property
|
|
def project_name(self) -> NormalizedName:
|
|
assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
|
|
return canonicalize_name(self._ireq.req.name)
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return format_name(self.project_name, self._extras)
|
|
|
|
def format_for_error(self) -> str:
|
|
# Convert comma-separated specifiers into "A, B, ..., F and G"
|
|
# This makes the specifier a bit more "human readable", without
|
|
# risking a change in meaning. (Hopefully! Not all edge cases have
|
|
# been checked)
|
|
parts = [s.strip() for s in str(self).split(",")]
|
|
if len(parts) == 0:
|
|
return ""
|
|
elif len(parts) == 1:
|
|
return parts[0]
|
|
|
|
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
|
|
|
def get_candidate_lookup(self) -> CandidateLookup:
|
|
return None, self._ireq
|
|
|
|
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
|
assert candidate.name == self.name, (
|
|
f"Internal issue: Candidate is not for this requirement "
|
|
f"{candidate.name} vs {self.name}"
|
|
)
|
|
# We can safely always allow prereleases here since PackageFinder
|
|
# already implements the prerelease logic, and would have filtered out
|
|
# prerelease candidates if the user does not expect them.
|
|
assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
|
|
spec = self._ireq.req.specifier
|
|
return spec.contains(candidate.version, prereleases=True)
|
|
|
|
|
|
class RequiresPythonRequirement(Requirement):
|
|
"""A requirement representing Requires-Python metadata."""
|
|
|
|
def __init__(self, specifier: SpecifierSet, match: Candidate) -> None:
|
|
self.specifier = specifier
|
|
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),
|
|
)
|
|
|
|
@property
|
|
def project_name(self) -> NormalizedName:
|
|
return self._candidate.project_name
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._candidate.name
|
|
|
|
def format_for_error(self) -> str:
|
|
return str(self)
|
|
|
|
def get_candidate_lookup(self) -> CandidateLookup:
|
|
if self.specifier.contains(self._candidate.version, prereleases=True):
|
|
return self._candidate, None
|
|
return None, None
|
|
|
|
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
|
assert candidate.name == self._candidate.name, "Not Python candidate"
|
|
# We can safely always allow prereleases here since PackageFinder
|
|
# already implements the prerelease logic, and would have filtered out
|
|
# prerelease candidates if the user does not expect them.
|
|
return self.specifier.contains(candidate.version, prereleases=True)
|
|
|
|
|
|
class UnsatisfiableRequirement(Requirement):
|
|
"""A requirement that cannot be satisfied."""
|
|
|
|
def __init__(self, name: NormalizedName) -> None:
|
|
self._name = name
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self._name} (unavailable)"
|
|
|
|
def __repr__(self) -> str:
|
|
return "{class_name}({name!r})".format(
|
|
class_name=self.__class__.__name__,
|
|
name=str(self._name),
|
|
)
|
|
|
|
@property
|
|
def project_name(self) -> NormalizedName:
|
|
return self._name
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._name
|
|
|
|
def format_for_error(self) -> str:
|
|
return str(self)
|
|
|
|
def get_candidate_lookup(self) -> CandidateLookup:
|
|
return None, None
|
|
|
|
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
|
return False
|