Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -6,7 +6,7 @@ __title__ = "packaging"
|
|||
__summary__ = "Core utilities for Python packages"
|
||||
__uri__ = "https://github.com/pypa/packaging"
|
||||
|
||||
__version__ = "26.0"
|
||||
__version__ = "26.2"
|
||||
|
||||
__author__ = "Donald Stufft and individual contributors"
|
||||
__email__ = "donald@stufft.io"
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -27,6 +27,34 @@ class Node:
|
|||
def serialize(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def __getstate__(self) -> str:
|
||||
# Return just the value string for compactness and stability.
|
||||
return self.value
|
||||
|
||||
def _restore_value(self, value: object) -> None:
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(
|
||||
f"Cannot restore {self.__class__.__name__} value from {value!r}"
|
||||
)
|
||||
self.value = value
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, str):
|
||||
# New format (26.2+): just the value string.
|
||||
self._restore_value(state)
|
||||
return
|
||||
if isinstance(state, tuple) and len(state) == 2:
|
||||
# Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
|
||||
_, slot_dict = state
|
||||
if isinstance(slot_dict, dict) and "value" in slot_dict:
|
||||
self._restore_value(slot_dict["value"])
|
||||
return
|
||||
if isinstance(state, dict) and "value" in state:
|
||||
# Old format (packaging <= 25.0, no __slots__): plain __dict__.
|
||||
self._restore_value(state["value"])
|
||||
return
|
||||
raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
|
||||
|
||||
|
||||
class Variable(Node):
|
||||
__slots__ = ()
|
||||
|
|
|
|||
|
|
@ -2,68 +2,32 @@
|
|||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
|
||||
import typing
|
||||
"""Backward-compatibility shim for unpickling Version objects serialized before
|
||||
packaging 26.1.
|
||||
|
||||
Old pickles reference ``packaging._structures.InfinityType`` and
|
||||
``packaging._structures.NegativeInfinityType``. This module provides minimal
|
||||
stand-in classes so that ``pickle.loads()`` can resolve those references.
|
||||
The deserialized objects are not used for comparisons — ``Version.__setstate__``
|
||||
discards the stale ``_key`` cache and recomputes it from the core version fields.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@typing.final
|
||||
class InfinityType:
|
||||
__slots__ = ()
|
||||
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Infinity"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(repr(self))
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __le__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
def __gt__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __ge__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __neg__(self: object) -> "NegativeInfinityType":
|
||||
return NegativeInfinity
|
||||
|
||||
|
||||
Infinity = InfinityType()
|
||||
|
||||
|
||||
@typing.final
|
||||
class NegativeInfinityType:
|
||||
__slots__ = ()
|
||||
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "-Infinity"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(repr(self))
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __le__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
def __gt__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __ge__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self: object) -> InfinityType:
|
||||
return Infinity
|
||||
|
||||
|
||||
Infinity = InfinityType()
|
||||
NegativeInfinity = NegativeInfinityType()
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ DEFAULT_RULES: dict[str, re.Pattern[str]] = {
|
|||
re.VERBOSE,
|
||||
),
|
||||
"SPECIFIER": re.compile(
|
||||
Specifier._operator_regex_str + Specifier._version_regex_str,
|
||||
Specifier._specifier_regex_str,
|
||||
re.VERBOSE | re.IGNORECASE,
|
||||
),
|
||||
"AT": re.compile(r"\@"),
|
||||
|
|
|
|||
302
venv/lib/python3.12/site-packages/packaging/dependency_groups.py
Normal file
302
venv/lib/python3.12/site-packages/packaging/dependency_groups.py
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from .errors import _ErrorCollector
|
||||
from .requirements import Requirement
|
||||
|
||||
__all__ = [
|
||||
"CyclicDependencyGroup",
|
||||
"DependencyGroupInclude",
|
||||
"DependencyGroupResolver",
|
||||
"DuplicateGroupNames",
|
||||
"InvalidDependencyGroupObject",
|
||||
"resolve_dependency_groups",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
# -----------
|
||||
# Error Types
|
||||
# -----------
|
||||
|
||||
|
||||
class DuplicateGroupNames(ValueError):
|
||||
"""
|
||||
The same dependency groups were defined twice, with different non-normalized names.
|
||||
"""
|
||||
|
||||
|
||||
class CyclicDependencyGroup(ValueError):
|
||||
"""
|
||||
The dependency group includes form a cycle.
|
||||
"""
|
||||
|
||||
def __init__(self, requested_group: str, group: str, include_group: str) -> None:
|
||||
self.requested_group = requested_group
|
||||
self.group = group
|
||||
self.include_group = include_group
|
||||
|
||||
if include_group == group:
|
||||
reason = f"{group} includes itself"
|
||||
else:
|
||||
reason = f"{include_group} -> {group}, {group} -> {include_group}"
|
||||
super().__init__(
|
||||
"Cyclic dependency group include while resolving "
|
||||
f"{requested_group}: {reason}"
|
||||
)
|
||||
|
||||
|
||||
# in the PEP 735 spec, the tables in dependency group lists were described as
|
||||
# "Dependency Object Specifiers", but the only defined type of object was a
|
||||
# "Dependency Group Include" -- hence the naming of this error as "Object"
|
||||
class InvalidDependencyGroupObject(ValueError):
|
||||
"""
|
||||
A member of a dependency group was identified as a dict, but was not in a valid
|
||||
format.
|
||||
"""
|
||||
|
||||
|
||||
# ------------------------
|
||||
# Object Model & Interface
|
||||
# ------------------------
|
||||
|
||||
|
||||
class DependencyGroupInclude:
|
||||
__slots__ = ("include_group",)
|
||||
|
||||
def __init__(self, include_group: str) -> None:
|
||||
"""
|
||||
Initialize a DependencyGroupInclude.
|
||||
|
||||
:param include_group: The name of the group referred to by this include.
|
||||
"""
|
||||
self.include_group = include_group
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.include_group!r})"
|
||||
|
||||
|
||||
class DependencyGroupResolver:
|
||||
"""
|
||||
A resolver for Dependency Group data.
|
||||
|
||||
This class handles caching, name normalization, cycle detection, and other
|
||||
parsing requirements. There are only two public methods for exploring the data:
|
||||
``lookup()`` and ``resolve()``.
|
||||
|
||||
:param dependency_groups: A mapping, as provided via pyproject
|
||||
``[dependency-groups]``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
|
||||
) -> None:
|
||||
errors = _ErrorCollector()
|
||||
|
||||
self.dependency_groups = _normalize_group_names(dependency_groups, errors)
|
||||
|
||||
# a map of group names to parsed data
|
||||
self._parsed_groups: dict[
|
||||
str, tuple[Requirement | DependencyGroupInclude, ...]
|
||||
] = {}
|
||||
# a map of group names to their ancestors, used for cycle detection
|
||||
self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}
|
||||
# a cache of completed resolutions to Requirement lists
|
||||
self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}
|
||||
|
||||
errors.finalize("[dependency-groups] data was invalid")
|
||||
|
||||
def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:
|
||||
"""
|
||||
Lookup a group name, returning the parsed dependency data for that group.
|
||||
This will not resolve includes.
|
||||
|
||||
:param group: the name of the group to lookup
|
||||
"""
|
||||
group = _normalize_name(group)
|
||||
|
||||
with _ErrorCollector().on_exit(
|
||||
f"[dependency-groups] data for {group!r} was malformed"
|
||||
) as errors:
|
||||
return self._parse_group(group, errors)
|
||||
|
||||
def resolve(self, group: str) -> tuple[Requirement, ...]:
|
||||
"""
|
||||
Resolve a dependency group to a list of requirements.
|
||||
|
||||
:param group: the name of the group to resolve
|
||||
"""
|
||||
group = _normalize_name(group)
|
||||
|
||||
with _ErrorCollector().on_exit(
|
||||
f"[dependency-groups] data for {group!r} was malformed"
|
||||
) as errors:
|
||||
return self._resolve(group, group, errors)
|
||||
|
||||
def _resolve(
|
||||
self, group: str, requested_group: str, errors: _ErrorCollector
|
||||
) -> tuple[Requirement, ...]:
|
||||
"""
|
||||
This is a helper for cached resolution to strings. It preserves the name of the
|
||||
group which the user initially requested in order to present a clearer error in
|
||||
the event that a cycle is detected.
|
||||
|
||||
:param group: The normalized name of the group to resolve.
|
||||
:param requested_group: The group which was used in the original, user-facing
|
||||
request.
|
||||
"""
|
||||
if group in self._resolve_cache:
|
||||
return self._resolve_cache[group]
|
||||
|
||||
parsed = self._parse_group(group, errors)
|
||||
|
||||
resolved_group = []
|
||||
|
||||
for item in parsed:
|
||||
if isinstance(item, Requirement):
|
||||
resolved_group.append(item)
|
||||
elif isinstance(item, DependencyGroupInclude):
|
||||
include_group = _normalize_name(item.include_group)
|
||||
|
||||
# if a group is cyclic, record the error
|
||||
# otherwise, follow the include_group reference
|
||||
#
|
||||
# this allows us to examine all includes in a group, even in the
|
||||
# presence of errors
|
||||
if include_group in self._include_graph_ancestors.get(group, ()):
|
||||
errors.error(
|
||||
CyclicDependencyGroup(
|
||||
requested_group, group, item.include_group
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._include_graph_ancestors[include_group] = (
|
||||
*self._include_graph_ancestors.get(group, ()),
|
||||
group,
|
||||
)
|
||||
resolved_group.extend(
|
||||
self._resolve(include_group, requested_group, errors)
|
||||
)
|
||||
else: # pragma: no cover
|
||||
raise NotImplementedError(
|
||||
f"Invalid dependency group item after parse: {item}"
|
||||
)
|
||||
|
||||
# in the event that errors were detected, present the group as empty and do not
|
||||
# cache the result
|
||||
# this ensures that repeated access to a cyclic group will raise multiple errors
|
||||
if errors.errors:
|
||||
return ()
|
||||
|
||||
self._resolve_cache[group] = tuple(resolved_group)
|
||||
return self._resolve_cache[group]
|
||||
|
||||
def _parse_group(
|
||||
self, group: str, errors: _ErrorCollector
|
||||
) -> tuple[Requirement | DependencyGroupInclude, ...]:
|
||||
# short circuit -- never do the work twice
|
||||
if group in self._parsed_groups:
|
||||
return self._parsed_groups[group]
|
||||
|
||||
if group not in self.dependency_groups:
|
||||
errors.error(LookupError(f"Dependency group '{group}' not found"))
|
||||
return ()
|
||||
|
||||
raw_group = self.dependency_groups[group]
|
||||
if isinstance(raw_group, str):
|
||||
errors.error(
|
||||
TypeError(
|
||||
f"Dependency group {group!r} contained a string rather than a list."
|
||||
)
|
||||
)
|
||||
return ()
|
||||
|
||||
if not isinstance(raw_group, Sequence):
|
||||
errors.error(
|
||||
TypeError(f"Dependency group {group!r} is not a sequence type.")
|
||||
)
|
||||
return ()
|
||||
|
||||
elements: list[Requirement | DependencyGroupInclude] = []
|
||||
for item in raw_group:
|
||||
if isinstance(item, str):
|
||||
# packaging.requirements.Requirement parsing ensures that this is a
|
||||
# valid PEP 508 Dependency Specifier
|
||||
# raises InvalidRequirement on failure
|
||||
elements.append(Requirement(item))
|
||||
elif isinstance(item, Mapping):
|
||||
if tuple(item.keys()) != ("include-group",):
|
||||
errors.error(
|
||||
InvalidDependencyGroupObject(
|
||||
f"Invalid dependency group item: {item!r}"
|
||||
)
|
||||
)
|
||||
else:
|
||||
include_group = item["include-group"]
|
||||
elements.append(DependencyGroupInclude(include_group=include_group))
|
||||
else:
|
||||
errors.error(TypeError(f"Invalid dependency group item: {item!r}"))
|
||||
|
||||
self._parsed_groups[group] = tuple(elements)
|
||||
return self._parsed_groups[group]
|
||||
|
||||
|
||||
# --------------------
|
||||
# Functional Interface
|
||||
# --------------------
|
||||
|
||||
|
||||
def resolve_dependency_groups(
|
||||
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]], /, *groups: str
|
||||
) -> tuple[str, ...]:
|
||||
"""
|
||||
Resolve a dependency group to a tuple of requirements, as strings.
|
||||
|
||||
:param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
|
||||
from ``pyproject.toml``
|
||||
:param groups: the name of the group(s) to resolve
|
||||
"""
|
||||
resolver = DependencyGroupResolver(dependency_groups)
|
||||
return tuple(str(r) for group in groups for r in resolver.resolve(group))
|
||||
|
||||
|
||||
# ----------------
|
||||
# internal helpers
|
||||
# ----------------
|
||||
|
||||
|
||||
_NORMALIZE_PATTERN = re.compile(r"[-_.]+")
|
||||
|
||||
|
||||
def _normalize_name(name: str) -> str:
|
||||
return _NORMALIZE_PATTERN.sub("-", name).lower()
|
||||
|
||||
|
||||
def _normalize_group_names(
|
||||
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
|
||||
errors: _ErrorCollector,
|
||||
) -> dict[str, Sequence[str | Mapping[str, str]]]:
|
||||
original_names: dict[str, list[str]] = {}
|
||||
normalized_groups: dict[str, Sequence[str | Mapping[str, str]]] = {}
|
||||
|
||||
for group_name, value in dependency_groups.items():
|
||||
normed_group_name = _normalize_name(group_name)
|
||||
original_names.setdefault(normed_group_name, []).append(group_name)
|
||||
normalized_groups[normed_group_name] = value
|
||||
|
||||
for normed_name, names in original_names.items():
|
||||
if len(names) > 1:
|
||||
errors.error(
|
||||
DuplicateGroupNames(
|
||||
"Duplicate dependency group names: "
|
||||
f"{normed_name} ({', '.join(names)})"
|
||||
)
|
||||
)
|
||||
|
||||
return normalized_groups
|
||||
325
venv/lib/python3.12/site-packages/packaging/direct_url.py
Normal file
325
venv/lib/python3.12/site-packages/packaging/direct_url.py
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
import urllib.parse
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import sys
|
||||
from collections.abc import Collection
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self
|
||||
else:
|
||||
from typing_extensions import Self
|
||||
|
||||
__all__ = [
|
||||
"ArchiveInfo",
|
||||
"DirInfo",
|
||||
"DirectUrl",
|
||||
"DirectUrlValidationError",
|
||||
"VcsInfo",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class _FromMappingProtocol(Protocol): # pragma: no cover
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
|
||||
|
||||
|
||||
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
|
||||
|
||||
|
||||
def _json_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||
return {key: value for key, value in data if value is not None}
|
||||
|
||||
|
||||
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
|
||||
"""Get a value from the dictionary and verify it's the expected type."""
|
||||
if (value := d.get(key)) is None:
|
||||
return None
|
||||
if not isinstance(value, expected_type):
|
||||
raise DirectUrlValidationError(
|
||||
f"Unexpected type {type(value).__name__} "
|
||||
f"(expected {expected_type.__name__})",
|
||||
context=key,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
|
||||
"""Get a required value from the dictionary and verify it's the expected type."""
|
||||
if (value := _get(d, expected_type, key)) is None:
|
||||
raise _DirectUrlRequiredKeyError(key)
|
||||
return value
|
||||
|
||||
|
||||
def _get_object(
|
||||
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
|
||||
) -> _FromMappingProtocolT | None:
|
||||
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
|
||||
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
|
||||
return None
|
||||
try:
|
||||
return target_type._from_dict(value)
|
||||
except Exception as e:
|
||||
raise DirectUrlValidationError(e, context=key) from e
|
||||
|
||||
|
||||
_PEP610_USER_PASS_ENV_VARS_REGEX = re.compile(
|
||||
r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$"
|
||||
)
|
||||
|
||||
|
||||
def _strip_auth_from_netloc(netloc: str, safe_user_passwords: Collection[str]) -> str:
|
||||
if "@" not in netloc:
|
||||
return netloc
|
||||
user_pass, netloc_no_user_pass = netloc.split("@", 1)
|
||||
if user_pass in safe_user_passwords:
|
||||
return netloc
|
||||
if _PEP610_USER_PASS_ENV_VARS_REGEX.match(user_pass):
|
||||
return netloc
|
||||
return netloc_no_user_pass
|
||||
|
||||
|
||||
def _strip_url(url: str, safe_user_passwords: Collection[str]) -> str:
|
||||
"""url with user:password part removed unless it is formed with
|
||||
environment variables as specified in PEP 610, or it is a safe user:password
|
||||
such as `git`.
|
||||
"""
|
||||
parsed_url = urllib.parse.urlsplit(url)
|
||||
netloc = _strip_auth_from_netloc(parsed_url.netloc, safe_user_passwords)
|
||||
return urllib.parse.urlunsplit(
|
||||
(
|
||||
parsed_url.scheme,
|
||||
netloc,
|
||||
parsed_url.path,
|
||||
parsed_url.query,
|
||||
parsed_url.fragment,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class DirectUrlValidationError(Exception):
|
||||
"""Raised when when input data is not spec-compliant."""
|
||||
|
||||
context: str | None = None
|
||||
message: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cause: str | Exception,
|
||||
*,
|
||||
context: str | None = None,
|
||||
) -> None:
|
||||
if isinstance(cause, DirectUrlValidationError):
|
||||
if cause.context:
|
||||
self.context = (
|
||||
f"{context}.{cause.context}" if context else cause.context
|
||||
)
|
||||
else:
|
||||
self.context = context # pragma: no cover
|
||||
self.message = cause.message
|
||||
else:
|
||||
self.context = context
|
||||
self.message = str(cause)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.context:
|
||||
return f"{self.message} in {self.context!r}"
|
||||
return self.message
|
||||
|
||||
|
||||
class _DirectUrlRequiredKeyError(DirectUrlValidationError):
|
||||
def __init__(self, key: str) -> None:
|
||||
super().__init__("Missing required value", context=key)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class VcsInfo:
|
||||
vcs: str
|
||||
commit_id: str
|
||||
requested_revision: str | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
vcs: str,
|
||||
commit_id: str,
|
||||
requested_revision: str | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, "vcs", vcs)
|
||||
object.__setattr__(self, "commit_id", commit_id)
|
||||
object.__setattr__(self, "requested_revision", requested_revision)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
# We can't validate vcs value because is not closed.
|
||||
return cls(
|
||||
vcs=_get_required(d, str, "vcs"),
|
||||
requested_revision=_get(d, str, "requested_revision"),
|
||||
commit_id=_get_required(d, str, "commit_id"),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class ArchiveInfo:
|
||||
hashes: Mapping[str, str] | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
hashes: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, "hashes", hashes)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
hashes = _get(d, Mapping, "hashes") # type: ignore[type-abstract]
|
||||
if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):
|
||||
raise DirectUrlValidationError(
|
||||
"Hash values must be strings", context="hashes"
|
||||
)
|
||||
legacy_hash = _get(d, str, "hash")
|
||||
if legacy_hash is not None:
|
||||
if "=" not in legacy_hash:
|
||||
raise DirectUrlValidationError(
|
||||
"Invalid hash format (expected '<algorithm>=<hash>')",
|
||||
context="hash",
|
||||
)
|
||||
hash_algorithm, hash_value = legacy_hash.split("=", 1)
|
||||
if hashes is None:
|
||||
# if `hashes` are not present, we can derive it from the legacy `hash`
|
||||
hashes = {hash_algorithm: hash_value}
|
||||
else:
|
||||
# if `hashes` are present, the legacy `hash` must match one of them
|
||||
if hash_algorithm not in hashes:
|
||||
raise DirectUrlValidationError(
|
||||
f"Algorithm {hash_algorithm!r} used in hash field "
|
||||
f"is not present in hashes field",
|
||||
context="hashes",
|
||||
)
|
||||
if hashes[hash_algorithm] != hash_value:
|
||||
raise DirectUrlValidationError(
|
||||
f"Algorithm {hash_algorithm!r} used in hash field "
|
||||
f"has different value in hashes field",
|
||||
context="hash",
|
||||
)
|
||||
return cls(hashes=hashes)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class DirInfo:
|
||||
editable: bool | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
editable: bool | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, "editable", editable)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
return cls(
|
||||
editable=_get(d, bool, "editable"),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class DirectUrl:
|
||||
"""A class representing a direct URL."""
|
||||
|
||||
url: str
|
||||
archive_info: ArchiveInfo | None = None
|
||||
vcs_info: VcsInfo | None = None
|
||||
dir_info: DirInfo | None = None
|
||||
subdirectory: str | None = None # XXX Path or str?
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
url: str,
|
||||
archive_info: ArchiveInfo | None = None,
|
||||
vcs_info: VcsInfo | None = None,
|
||||
dir_info: DirInfo | None = None,
|
||||
subdirectory: str | None = None,
|
||||
) -> None:
|
||||
object.__setattr__(self, "url", url)
|
||||
object.__setattr__(self, "archive_info", archive_info)
|
||||
object.__setattr__(self, "vcs_info", vcs_info)
|
||||
object.__setattr__(self, "dir_info", dir_info)
|
||||
object.__setattr__(self, "subdirectory", subdirectory)
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
|
||||
direct_url = cls(
|
||||
url=_get_required(d, str, "url"),
|
||||
archive_info=_get_object(d, ArchiveInfo, "archive_info"),
|
||||
vcs_info=_get_object(d, VcsInfo, "vcs_info"),
|
||||
dir_info=_get_object(d, DirInfo, "dir_info"),
|
||||
subdirectory=_get(d, str, "subdirectory"),
|
||||
)
|
||||
if (
|
||||
bool(direct_url.vcs_info)
|
||||
+ bool(direct_url.archive_info)
|
||||
+ bool(direct_url.dir_info)
|
||||
) != 1:
|
||||
raise DirectUrlValidationError(
|
||||
"Exactly one of vcs_info, archive_info, dir_info must be present"
|
||||
)
|
||||
if direct_url.dir_info is not None and not direct_url.url.startswith("file://"):
|
||||
raise DirectUrlValidationError(
|
||||
"URL scheme must be file:// when dir_info is present",
|
||||
context="url",
|
||||
)
|
||||
# XXX subdirectory must be relative, can we, should we validate that here?
|
||||
return direct_url
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
|
||||
"""Create and validate a DirectUrl instance from a JSON dictionary."""
|
||||
return cls._from_dict(d)
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
*,
|
||||
generate_legacy_hash: bool = False,
|
||||
strip_user_password: bool = True,
|
||||
safe_user_passwords: Collection[str] = ("git",),
|
||||
) -> Mapping[str, Any]:
|
||||
"""Convert the DirectUrl instance to a JSON dictionary.
|
||||
|
||||
:param generate_legacy_hash: If True, include a legacy `hash` field in
|
||||
`archive_info` for backward compatibility with tools that don't
|
||||
support the `hashes` field.
|
||||
:param strip_user_password: If True, strip user:password from the URL
|
||||
unless it is formed with environment variables as specified in PEP
|
||||
610, or it is a safe user:password such as `git`.
|
||||
:param safe_user_passwords: A collection of user:password strings that
|
||||
should not be stripped from the URL even if `strip_user_password` is
|
||||
True.
|
||||
"""
|
||||
res = dataclasses.asdict(self, dict_factory=_json_dict_factory)
|
||||
if generate_legacy_hash and self.archive_info and self.archive_info.hashes:
|
||||
hash_algorithm, hash_value = next(iter(self.archive_info.hashes.items()))
|
||||
res["archive_info"]["hash"] = f"{hash_algorithm}={hash_value}"
|
||||
if strip_user_password:
|
||||
res["url"] = _strip_url(self.url, safe_user_passwords)
|
||||
return res
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate the DirectUrl instance against the specification.
|
||||
|
||||
Raises :class:`DirectUrlValidationError` if invalid.
|
||||
"""
|
||||
self.from_dict(self.to_dict())
|
||||
94
venv/lib/python3.12/site-packages/packaging/errors.py
Normal file
94
venv/lib/python3.12/site-packages/packaging/errors.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import sys
|
||||
import typing
|
||||
|
||||
__all__ = ["ExceptionGroup"]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11): # pragma: no cover
|
||||
from builtins import ExceptionGroup
|
||||
else: # pragma: no cover
|
||||
|
||||
class ExceptionGroup(Exception):
|
||||
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
|
||||
|
||||
If :external:exc:`ExceptionGroup` is already defined by Python itself,
|
||||
that version is used instead.
|
||||
"""
|
||||
|
||||
message: str
|
||||
exceptions: list[Exception]
|
||||
|
||||
def __init__(self, message: str, exceptions: list[Exception]) -> None:
|
||||
self.message = message
|
||||
self.exceptions = exceptions
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _ErrorCollector:
|
||||
"""
|
||||
Collect errors into ExceptionGroups.
|
||||
|
||||
Used like this:
|
||||
|
||||
collector = _ErrorCollector()
|
||||
# Add a single exception
|
||||
collector.error(ValueError("one"))
|
||||
|
||||
# Supports nesting, including combining ExceptionGroups
|
||||
with collector.collect():
|
||||
raise ValueError("two")
|
||||
collector.finalize("Found some errors")
|
||||
|
||||
Since making a collector and then calling finalize later is a common pattern,
|
||||
a convenience method ``on_exit`` is provided.
|
||||
"""
|
||||
|
||||
errors: list[Exception] = dataclasses.field(default_factory=list, init=False)
|
||||
|
||||
def finalize(self, msg: str) -> None:
|
||||
"""Raise a group exception if there are any errors."""
|
||||
if self.errors:
|
||||
raise ExceptionGroup(msg, self.errors)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]:
|
||||
"""
|
||||
Calls finalize if no uncollected errors were present.
|
||||
|
||||
Uncollected errors are raised normally.
|
||||
"""
|
||||
yield self
|
||||
self.finalize(msg)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def collect(self, *err_cls: type[Exception]) -> typing.Generator[None, None, None]:
|
||||
"""
|
||||
Context manager to collect errors into the error list.
|
||||
|
||||
Must be inside loops, as only one error can be collected at a time.
|
||||
"""
|
||||
error_classes = err_cls or (Exception,)
|
||||
try:
|
||||
yield
|
||||
except ExceptionGroup as error:
|
||||
self.errors.extend(error.exceptions)
|
||||
except error_classes as error:
|
||||
self.errors.append(error)
|
||||
|
||||
def error(
|
||||
self,
|
||||
error: Exception,
|
||||
) -> None:
|
||||
"""Add an error to the list."""
|
||||
self.errors.append(error)
|
||||
|
|
@ -42,14 +42,25 @@ __all__ = [
|
|||
"canonicalize_license_expression",
|
||||
]
|
||||
|
||||
|
||||
# Simple __dir__ implementation since there are no public submodules
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
|
||||
|
||||
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
|
||||
"""
|
||||
A :class:`typing.NewType` of :class:`str`, representing a normalized
|
||||
License-Expression.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidLicenseExpression(ValueError):
|
||||
"""Raised when a license-expression string is invalid
|
||||
|
||||
>>> from packaging.licenses import canonicalize_license_expression
|
||||
>>> canonicalize_license_expression("invalid")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
|
|
@ -60,6 +71,34 @@ class InvalidLicenseExpression(ValueError):
|
|||
def canonicalize_license_expression(
|
||||
raw_license_expression: str,
|
||||
) -> NormalizedLicenseExpression:
|
||||
"""
|
||||
This function takes a valid License-Expression, and returns the normalized
|
||||
form of it.
|
||||
|
||||
The return type is typed as :class:`NormalizedLicenseExpression`. This
|
||||
allows type checkers to help require that a string has passed through this
|
||||
function before use.
|
||||
|
||||
:param str raw_license_expression: The License-Expression to canonicalize.
|
||||
:raises InvalidLicenseExpression: If the License-Expression is invalid due to an
|
||||
invalid/unknown license identifier or invalid syntax.
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from packaging.licenses import canonicalize_license_expression
|
||||
>>> canonicalize_license_expression("mit")
|
||||
'MIT'
|
||||
>>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
|
||||
'MIT AND (Apache-2.0 OR BSD-2-Clause)'
|
||||
>>> canonicalize_license_expression("(mit")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
InvalidLicenseExpression: Invalid license expression: '(mit'
|
||||
>>> canonicalize_license_expression("Use-it-after-midnight")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
|
||||
"""
|
||||
if not raw_license_expression:
|
||||
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||
raise InvalidLicenseExpression(message)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -26,8 +26,22 @@ __all__ = [
|
|||
"default_environment",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
|
||||
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
|
||||
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
|
||||
|
||||
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
|
||||
|
||||
* ``"metadata"`` (for core metadata; default)
|
||||
* ``"lock_file"`` (for lock files)
|
||||
* ``"requirement"`` (i.e. all other situations)
|
||||
"""
|
||||
|
||||
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
|
||||
MARKERS_REQUIRING_VERSION = {
|
||||
"implementation_version",
|
||||
|
|
@ -38,25 +52,32 @@ MARKERS_REQUIRING_VERSION = {
|
|||
|
||||
|
||||
class InvalidMarker(ValueError):
|
||||
"""
|
||||
An invalid marker was found, users should refer to PEP 508.
|
||||
"""Raised when attempting to create a :class:`Marker` from invalid input.
|
||||
|
||||
This error indicates that the given marker string does not conform to the
|
||||
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||
"""
|
||||
|
||||
|
||||
class UndefinedComparison(ValueError):
|
||||
"""
|
||||
An invalid operation was attempted on a value that doesn't support it.
|
||||
"""Raised when evaluating an unsupported marker comparison.
|
||||
|
||||
This can happen when marker values are compared as versions but do not
|
||||
conform to the :ref:`specification of version specifiers
|
||||
<pypug:version-specifiers>`.
|
||||
"""
|
||||
|
||||
|
||||
class UndefinedEnvironmentName(ValueError):
|
||||
"""
|
||||
A name was attempted to be used that does not exist inside of the
|
||||
environment.
|
||||
"""
|
||||
"""Raised when evaluating a marker that references a missing environment key."""
|
||||
|
||||
|
||||
class Environment(TypedDict):
|
||||
"""
|
||||
A dictionary that represents a Python environment as captured by
|
||||
:func:`default_environment`. All fields are required.
|
||||
"""
|
||||
|
||||
implementation_name: str
|
||||
"""The implementation's identifier, e.g. ``'cpython'``."""
|
||||
|
||||
|
|
@ -263,7 +284,7 @@ def _evaluate_markers(
|
|||
return any(all(item) for item in groups)
|
||||
|
||||
|
||||
def format_full_version(info: sys._version_info) -> str:
|
||||
def _format_full_version(info: sys._version_info) -> str:
|
||||
version = f"{info.major}.{info.minor}.{info.micro}"
|
||||
kind = info.releaselevel
|
||||
if kind != "final":
|
||||
|
|
@ -272,7 +293,11 @@ def format_full_version(info: sys._version_info) -> str:
|
|||
|
||||
|
||||
def default_environment() -> Environment:
|
||||
iver = format_full_version(sys.implementation.version)
|
||||
"""Return the default marker environment for the current Python process.
|
||||
|
||||
This is the base environment used by :meth:`Marker.evaluate`.
|
||||
"""
|
||||
iver = _format_full_version(sys.implementation.version)
|
||||
implementation_name = sys.implementation.name
|
||||
return {
|
||||
"implementation_name": implementation_name,
|
||||
|
|
@ -290,6 +315,27 @@ def default_environment() -> Environment:
|
|||
|
||||
|
||||
class Marker:
|
||||
"""Represents a parsed dependency marker expression.
|
||||
|
||||
Marker expressions are parsed according to the
|
||||
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
|
||||
|
||||
:param marker: The string representation of a marker expression.
|
||||
:raises InvalidMarker: If ``marker`` cannot be parsed.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
.. versionchanged:: 26.2
|
||||
|
||||
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
"""
|
||||
|
||||
__slots__ = ("_markers",)
|
||||
|
||||
def __init__(self, marker: str) -> None:
|
||||
# Note: We create a Marker object without calling this constructor in
|
||||
# packaging.requirements.Requirement. If any additional logic is
|
||||
|
|
@ -320,11 +366,21 @@ class Marker:
|
|||
except ParserSyntaxError as e:
|
||||
raise InvalidMarker(str(e)) from e
|
||||
|
||||
@classmethod
|
||||
def _from_markers(cls, markers: MarkerList) -> Marker:
|
||||
"""Create a Marker instance from a pre-parsed marker tree.
|
||||
|
||||
This avoids re-parsing serialised marker strings when combining markers.
|
||||
"""
|
||||
new = cls.__new__(cls)
|
||||
new._markers = markers
|
||||
return new
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _format_marker(self._markers)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}('{self}')>"
|
||||
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(str(self))
|
||||
|
|
@ -335,6 +391,45 @@ class Marker:
|
|||
|
||||
return str(self) == str(other)
|
||||
|
||||
def __getstate__(self) -> str:
|
||||
# Return the marker expression string for compactness and stability.
|
||||
# Internal Node objects are excluded; the string is re-parsed on load.
|
||||
return str(self)
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, str):
|
||||
# New format (26.2+): just the marker expression string.
|
||||
try:
|
||||
self._markers = _normalize_extra_values(_parse_marker(state))
|
||||
except ParserSyntaxError as exc:
|
||||
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
|
||||
return
|
||||
if isinstance(state, dict) and "_markers" in state:
|
||||
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||
markers = state["_markers"]
|
||||
if isinstance(markers, list):
|
||||
self._markers = markers
|
||||
return
|
||||
if isinstance(state, tuple) and len(state) == 2:
|
||||
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
|
||||
_, slot_dict = state
|
||||
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
|
||||
markers = slot_dict["_markers"]
|
||||
if isinstance(markers, list):
|
||||
self._markers = markers
|
||||
return
|
||||
raise TypeError(f"Cannot restore Marker from {state!r}")
|
||||
|
||||
def __and__(self, other: Marker) -> Marker:
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
return self._from_markers([self._markers, "and", other._markers])
|
||||
|
||||
def __or__(self, other: Marker) -> Marker:
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
return self._from_markers([self._markers, "or", other._markers])
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
environment: Mapping[str, str | AbstractSet[str]] | None = None,
|
||||
|
|
@ -342,14 +437,23 @@ class Marker:
|
|||
) -> bool:
|
||||
"""Evaluate a marker.
|
||||
|
||||
Return the boolean from evaluating the given marker against the
|
||||
environment. environment is an optional argument to override all or
|
||||
part of the determined environment. The *context* parameter specifies what
|
||||
context the markers are being evaluated for, which influences what markers
|
||||
are considered valid. Acceptable values are "metadata" (for core metadata;
|
||||
default), "lock_file", and "requirement" (i.e. all other situations).
|
||||
Return the boolean from evaluating this marker against the environment.
|
||||
The environment is determined from the current Python process unless
|
||||
passed in explicitly.
|
||||
|
||||
:param environment: Mapping containing keys and values to override the
|
||||
detected environment.
|
||||
:param EvaluateContext context: The context in which the marker is
|
||||
evaluated, which influences what marker names are considered valid.
|
||||
Accepted values are ``"metadata"`` (for core metadata; default),
|
||||
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
|
||||
:raises UndefinedComparison: If the marker uses a comparison on values
|
||||
that are not valid versions per the :ref:`specification of version
|
||||
specifiers <pypug:version-specifiers>`.
|
||||
:raises UndefinedEnvironmentName: If the marker references a value that
|
||||
is missing from the evaluation environment.
|
||||
:returns: ``True`` if the marker matches, otherwise ``False``.
|
||||
|
||||
The environment is determined from the current Python process.
|
||||
"""
|
||||
current_environment = cast(
|
||||
"dict[str, str | AbstractSet[str]]", default_environment()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import email.feedparser
|
||||
import email.header
|
||||
import email.message
|
||||
import email.parser
|
||||
import email.policy
|
||||
import keyword
|
||||
import pathlib
|
||||
import sys
|
||||
import typing
|
||||
from typing import (
|
||||
Any,
|
||||
|
|
@ -20,6 +18,7 @@ from typing import (
|
|||
|
||||
from . import licenses, requirements, specifiers, utils
|
||||
from . import version as version_module
|
||||
from .errors import ExceptionGroup, _ErrorCollector
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .licenses import NormalizedLicenseExpression
|
||||
|
|
@ -27,26 +26,19 @@ if typing.TYPE_CHECKING:
|
|||
T = typing.TypeVar("T")
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11): # pragma: no cover
|
||||
ExceptionGroup = ExceptionGroup # noqa: F821
|
||||
else: # pragma: no cover
|
||||
__all__ = [
|
||||
"ExceptionGroup", # Keep this for a bit (makes mypy happy w/ 26.0 compat)
|
||||
"InvalidMetadata",
|
||||
"Metadata",
|
||||
"RFC822Message",
|
||||
"RFC822Policy",
|
||||
"RawMetadata",
|
||||
"parse_email",
|
||||
]
|
||||
|
||||
class ExceptionGroup(Exception):
|
||||
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
|
||||
|
||||
If :external:exc:`ExceptionGroup` is already defined by Python itself,
|
||||
that version is used instead.
|
||||
"""
|
||||
|
||||
message: str
|
||||
exceptions: list[Exception]
|
||||
|
||||
def __init__(self, message: str, exceptions: list[Exception]) -> None:
|
||||
self.message = message
|
||||
self.exceptions = exceptions
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
class InvalidMetadata(ValueError):
|
||||
|
|
@ -76,8 +68,8 @@ class RawMetadata(TypedDict, total=False):
|
|||
|
||||
Core metadata fields that can be specified multiple times are stored as a
|
||||
list or dict depending on which is appropriate for the field. Any fields
|
||||
which hold multiple values in a single field are stored as a list.
|
||||
|
||||
which hold multiple values in a single field are stored as a list. All fields
|
||||
are considered optional.
|
||||
"""
|
||||
|
||||
# Metadata 1.0 - PEP 241
|
||||
|
|
@ -641,7 +633,7 @@ class _Validator(Generic[T]):
|
|||
charset = parameters.get("charset", "UTF-8")
|
||||
if charset != "UTF-8":
|
||||
raise self._invalid_metadata(
|
||||
f"{{field}} can only specify the UTF-8 charset, not {list(charset)}"
|
||||
f"{{field}} can only specify the UTF-8 charset, not {charset!r}"
|
||||
)
|
||||
|
||||
markdown_variants = {"GFM", "CommonMark"}
|
||||
|
|
@ -784,13 +776,11 @@ class Metadata:
|
|||
ins._raw = data.copy() # Mutations occur due to caching enriched values.
|
||||
|
||||
if validate:
|
||||
exceptions: list[Exception] = []
|
||||
try:
|
||||
collector = _ErrorCollector()
|
||||
metadata_version = None
|
||||
with collector.collect(InvalidMetadata):
|
||||
metadata_version = ins.metadata_version
|
||||
metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
|
||||
except InvalidMetadata as metadata_version_exc:
|
||||
exceptions.append(metadata_version_exc)
|
||||
metadata_version = None
|
||||
|
||||
# Make sure to check for the fields that are present, the required
|
||||
# fields (so their absence can be reported).
|
||||
|
|
@ -807,7 +797,7 @@ class Metadata:
|
|||
field_metadata_version = cls.__dict__[key].added
|
||||
except KeyError:
|
||||
exc = InvalidMetadata(key, f"unrecognized field: {key!r}")
|
||||
exceptions.append(exc)
|
||||
collector.error(exc)
|
||||
continue
|
||||
field_age = _VALID_METADATA_VERSIONS.index(
|
||||
field_metadata_version
|
||||
|
|
@ -819,14 +809,13 @@ class Metadata:
|
|||
f"{field} introduced in metadata version "
|
||||
f"{field_metadata_version}, not {metadata_version}",
|
||||
)
|
||||
exceptions.append(exc)
|
||||
collector.error(exc)
|
||||
continue
|
||||
getattr(ins, key)
|
||||
except InvalidMetadata as exc:
|
||||
exceptions.append(exc)
|
||||
collector.error(exc)
|
||||
|
||||
if exceptions:
|
||||
raise ExceptionGroup("invalid metadata", exceptions)
|
||||
collector.finalize("invalid metadata")
|
||||
|
||||
return ins
|
||||
|
||||
|
|
@ -840,16 +829,13 @@ class Metadata:
|
|||
raw, unparsed = parse_email(data)
|
||||
|
||||
if validate:
|
||||
exceptions: list[Exception] = []
|
||||
for unparsed_key in unparsed:
|
||||
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
|
||||
message = f"{unparsed_key!r} has invalid data"
|
||||
else:
|
||||
message = f"unrecognized field: {unparsed_key!r}"
|
||||
exceptions.append(InvalidMetadata(unparsed_key, message))
|
||||
|
||||
if exceptions:
|
||||
raise ExceptionGroup("unparsed", exceptions)
|
||||
with _ErrorCollector().on_exit("unparsed") as collector:
|
||||
for unparsed_key in unparsed:
|
||||
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
|
||||
message = f"{unparsed_key!r} has invalid data"
|
||||
else:
|
||||
message = f"unrecognized field: {unparsed_key!r}"
|
||||
collector.error(InvalidMetadata(unparsed_key, message))
|
||||
|
||||
try:
|
||||
return cls.from_raw(raw, validate=validate)
|
||||
|
|
|
|||
|
|
@ -12,18 +12,29 @@ from typing import (
|
|||
Callable,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .markers import Marker
|
||||
from .markers import Environment, Marker, default_environment
|
||||
from .specifiers import SpecifierSet
|
||||
from .utils import NormalizedName, is_normalized_name
|
||||
from .tags import create_compatible_tags_selector, sys_tags
|
||||
from .utils import (
|
||||
NormalizedName,
|
||||
is_normalized_name,
|
||||
parse_sdist_filename,
|
||||
parse_wheel_filename,
|
||||
)
|
||||
from .version import Version
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from collections.abc import Collection, Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from .tags import Tag
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -39,6 +50,11 @@ __all__ = [
|
|||
"is_valid_pylock_path",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T2 = TypeVar("_T2")
|
||||
|
||||
|
|
@ -222,6 +238,26 @@ def _validate_path_url(path: str | None, url: str | None) -> None:
|
|||
raise PylockValidationError("path or url must be provided")
|
||||
|
||||
|
||||
def _path_name(path: str | None) -> str | None:
|
||||
if not path:
|
||||
return None
|
||||
# If the path is relative it MAY use POSIX-style path separators explicitly
|
||||
# for portability
|
||||
if "/" in path:
|
||||
return path.rsplit("/", 1)[-1]
|
||||
elif "\\" in path:
|
||||
return path.rsplit("\\", 1)[-1]
|
||||
else:
|
||||
return path
|
||||
|
||||
|
||||
def _url_name(url: str | None) -> str | None:
|
||||
if not url:
|
||||
return None
|
||||
url_path = urlparse(url).path
|
||||
return url_path.rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if not hashes:
|
||||
raise PylockValidationError("At least one hash must be provided")
|
||||
|
|
@ -269,6 +305,10 @@ class PylockUnsupportedVersionError(PylockValidationError):
|
|||
"""Raised when encountering an unsupported `lock_version`."""
|
||||
|
||||
|
||||
class PylockSelectError(Exception):
|
||||
"""Base exception for errors raised by :meth:`Pylock.select`."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class PackageVcs:
|
||||
type: str
|
||||
|
|
@ -418,6 +458,14 @@ class PackageSdist:
|
|||
_validate_path_url(package_sdist.path, package_sdist.url)
|
||||
return package_sdist
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
"""Get the filename of the sdist."""
|
||||
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||
if not filename:
|
||||
raise PylockValidationError("Cannot determine sdist filename")
|
||||
return filename
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class PackageWheel:
|
||||
|
|
@ -459,6 +507,14 @@ class PackageWheel:
|
|||
_validate_path_url(package_wheel.path, package_wheel.url)
|
||||
return package_wheel
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
"""Get the filename of the wheel."""
|
||||
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||
if not filename:
|
||||
raise PylockValidationError("Cannot determine wheel filename")
|
||||
return filename
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
class Package:
|
||||
|
|
@ -538,6 +594,46 @@ class Package:
|
|||
"Exactly one of vcs, directory, archive must be set "
|
||||
"if sdist and wheels are not set"
|
||||
)
|
||||
for i, wheel in enumerate(package.wheels or []):
|
||||
try:
|
||||
(name, version, _, _) = parse_wheel_filename(wheel.filename)
|
||||
except Exception as e:
|
||||
raise PylockValidationError(
|
||||
f"Invalid wheel filename {wheel.filename!r}",
|
||||
context=f"wheels[{i}]",
|
||||
) from e
|
||||
if name != package.name:
|
||||
raise PylockValidationError(
|
||||
f"Name in {wheel.filename!r} is not consistent with "
|
||||
f"package name {package.name!r}",
|
||||
context=f"wheels[{i}]",
|
||||
)
|
||||
if package.version and version != package.version:
|
||||
raise PylockValidationError(
|
||||
f"Version in {wheel.filename!r} is not consistent with "
|
||||
f"package version {str(package.version)!r}",
|
||||
context=f"wheels[{i}]",
|
||||
)
|
||||
if package.sdist:
|
||||
try:
|
||||
name, version = parse_sdist_filename(package.sdist.filename)
|
||||
except Exception as e:
|
||||
raise PylockValidationError(
|
||||
f"Invalid sdist filename {package.sdist.filename!r}",
|
||||
context="sdist",
|
||||
) from e
|
||||
if name != package.name:
|
||||
raise PylockValidationError(
|
||||
f"Name in {package.sdist.filename!r} is not consistent with "
|
||||
f"package name {package.name!r}",
|
||||
context="sdist",
|
||||
)
|
||||
if package.version and version != package.version:
|
||||
raise PylockValidationError(
|
||||
f"Version in {package.sdist.filename!r} is not consistent with "
|
||||
f"package version {str(package.version)!r}",
|
||||
context="sdist",
|
||||
)
|
||||
try:
|
||||
for i, attestation_identity in enumerate( # noqa: B007
|
||||
package.attestation_identities or []
|
||||
|
|
@ -633,3 +729,177 @@ class Pylock:
|
|||
|
||||
Raises :class:`PylockValidationError` otherwise."""
|
||||
self.from_dict(self.to_dict())
|
||||
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
environment: Environment | None = None,
|
||||
tags: Sequence[Tag] | None = None,
|
||||
extras: Collection[str] | None = None,
|
||||
dependency_groups: Collection[str] | None = None,
|
||||
) -> Iterator[
|
||||
tuple[
|
||||
Package,
|
||||
PackageVcs
|
||||
| PackageDirectory
|
||||
| PackageArchive
|
||||
| PackageWheel
|
||||
| PackageSdist,
|
||||
]
|
||||
]:
|
||||
"""Select what to install from the lock file.
|
||||
|
||||
The *environment* and *tags* parameters represent the environment being
|
||||
selected for. If unspecified, ``packaging.markers.default_environment()`` and
|
||||
``packaging.tags.sys_tags()`` are used.
|
||||
|
||||
The *extras* parameter represents the extras to install.
|
||||
|
||||
The *dependency_groups* parameter represents the groups to install. If
|
||||
unspecified, the default groups are used.
|
||||
|
||||
This method must be used on valid Pylock instances (i.e. one obtained
|
||||
from :meth:`Pylock.from_dict` or if constructed manually, after calling
|
||||
:meth:`Pylock.validate`).
|
||||
"""
|
||||
compatible_tags_selector = create_compatible_tags_selector(tags or sys_tags())
|
||||
|
||||
# #. Gather the extras and dependency groups to install and set ``extras`` and
|
||||
# ``dependency_groups`` for marker evaluation, respectively.
|
||||
#
|
||||
# #. ``extras`` SHOULD be set to the empty set by default.
|
||||
# #. ``dependency_groups`` SHOULD be the set created from
|
||||
# :ref:`pylock-default-groups` by default.
|
||||
env = cast(
|
||||
"dict[str, str | frozenset[str]]",
|
||||
dict(
|
||||
environment or {}, # Marker.evaluate will fill-up
|
||||
extras=frozenset(extras or []),
|
||||
dependency_groups=frozenset(
|
||||
(self.default_groups or [])
|
||||
if dependency_groups is None # to allow selecting no group
|
||||
else dependency_groups
|
||||
),
|
||||
),
|
||||
)
|
||||
env_python_full_version = (
|
||||
environment["python_full_version"]
|
||||
if environment
|
||||
else default_environment()["python_full_version"]
|
||||
)
|
||||
|
||||
# #. Check if the metadata version specified by :ref:`pylock-lock-version` is
|
||||
# supported; an error or warning MUST be raised as appropriate.
|
||||
# Covered by lock.validate() which is a precondition for this method.
|
||||
|
||||
# #. If :ref:`pylock-requires-python` is specified, check that the environment
|
||||
# being installed for meets the requirement; an error MUST be raised if it is
|
||||
# not met.
|
||||
if self.requires_python and not self.requires_python.contains(
|
||||
env_python_full_version,
|
||||
):
|
||||
raise PylockSelectError(
|
||||
f"python_full_version {env_python_full_version!r} "
|
||||
f"in provided environment does not satisfy the Python version "
|
||||
f"requirement {str(self.requires_python)!r}"
|
||||
)
|
||||
|
||||
# #. If :ref:`pylock-environments` is specified, check that at least one of the
|
||||
# environment marker expressions is satisfied; an error MUST be raised if no
|
||||
# expression is satisfied.
|
||||
if self.environments:
|
||||
for env_marker in self.environments:
|
||||
if env_marker.evaluate(
|
||||
cast("dict[str, str]", environment or {}), context="requirement"
|
||||
):
|
||||
break
|
||||
else:
|
||||
raise PylockSelectError(
|
||||
"Provided environment does not satisfy any of the "
|
||||
"environments specified in the lock file"
|
||||
)
|
||||
|
||||
# #. For each package listed in :ref:`pylock-packages`:
|
||||
selected_packages_by_name: dict[str, tuple[int, Package]] = {}
|
||||
for package_index, package in enumerate(self.packages):
|
||||
# #. If :ref:`pylock-packages-marker` is specified, check if it is
|
||||
# satisfied;if it isn't, skip to the next package.
|
||||
if package.marker and not package.marker.evaluate(env, context="lock_file"):
|
||||
continue
|
||||
|
||||
# #. If :ref:`pylock-packages-requires-python` is specified, check if it is
|
||||
# satisfied; an error MUST be raised if it isn't.
|
||||
if package.requires_python and not package.requires_python.contains(
|
||||
env_python_full_version,
|
||||
):
|
||||
raise PylockSelectError(
|
||||
f"python_full_version {env_python_full_version!r} "
|
||||
f"in provided environment does not satisfy the Python version "
|
||||
f"requirement {str(package.requires_python)!r} for package "
|
||||
f"{package.name!r} at packages[{package_index}]"
|
||||
)
|
||||
|
||||
# #. Check that no other conflicting instance of the package has been slated
|
||||
# to be installed; an error about the ambiguity MUST be raised otherwise.
|
||||
if package.name in selected_packages_by_name:
|
||||
raise PylockSelectError(
|
||||
f"Multiple packages with the name {package.name!r} are "
|
||||
f"selected at packages[{package_index}] and "
|
||||
f"packages[{selected_packages_by_name[package.name][0]}]"
|
||||
)
|
||||
|
||||
# #. Check that the source of the package is specified appropriately (i.e.
|
||||
# there are no conflicting sources in the package entry);
|
||||
# an error MUST be raised if any issues are found.
|
||||
# Covered by lock.validate() which is a precondition for this method.
|
||||
|
||||
# #. Add the package to the set of packages to install.
|
||||
selected_packages_by_name[package.name] = (package_index, package)
|
||||
|
||||
# #. For each package to be installed:
|
||||
for package_index, package in selected_packages_by_name.values():
|
||||
# - If :ref:`pylock-packages-vcs` is set:
|
||||
if package.vcs is not None:
|
||||
yield package, package.vcs
|
||||
|
||||
# - Else if :ref:`pylock-packages-directory` is set:
|
||||
elif package.directory is not None:
|
||||
yield package, package.directory
|
||||
|
||||
# - Else if :ref:`pylock-packages-archive` is set:
|
||||
elif package.archive is not None:
|
||||
yield package, package.archive
|
||||
|
||||
# - Else if there are entries for :ref:`pylock-packages-wheels`:
|
||||
elif package.wheels:
|
||||
# #. Look for the appropriate wheel file based on
|
||||
# :ref:`pylock-packages-wheels-name`; if one is not found then move
|
||||
# on to :ref:`pylock-packages-sdist` or an error MUST be raised about
|
||||
# a lack of source for the project.
|
||||
best_wheel = next(
|
||||
compatible_tags_selector(
|
||||
(wheel, parse_wheel_filename(wheel.filename)[-1])
|
||||
for wheel in package.wheels
|
||||
),
|
||||
None,
|
||||
)
|
||||
if best_wheel:
|
||||
yield package, best_wheel
|
||||
elif package.sdist is not None:
|
||||
yield package, package.sdist
|
||||
else:
|
||||
raise PylockSelectError(
|
||||
f"No wheel found matching the provided tags "
|
||||
f"for package {package.name!r} "
|
||||
f"at packages[{package_index}], "
|
||||
f"and no sdist available as a fallback"
|
||||
)
|
||||
|
||||
# - Else if no :ref:`pylock-packages-wheels` file is found or
|
||||
# :ref:`pylock-packages-sdist` is solely set:
|
||||
elif package.sdist is not None:
|
||||
yield package, package.sdist
|
||||
|
||||
else:
|
||||
# Covered by lock.validate() which is a precondition for this method.
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -11,6 +11,15 @@ from .markers import Marker, _normalize_extra_values
|
|||
from .specifiers import SpecifierSet
|
||||
from .utils import canonicalize_name
|
||||
|
||||
__all__ = [
|
||||
"InvalidRequirement",
|
||||
"Requirement",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
class InvalidRequirement(ValueError):
|
||||
"""
|
||||
|
|
@ -24,6 +33,16 @@ class Requirement:
|
|||
Parse a given requirement string into its parts, such as name, specifier,
|
||||
URL, and extras. Raises InvalidRequirement on a badly-formed requirement
|
||||
string.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
.. versionchanged:: 26.2
|
||||
|
||||
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
"""
|
||||
|
||||
# TODO: Can we test whether something is contained within a requirement?
|
||||
|
|
@ -64,11 +83,35 @@ class Requirement:
|
|||
if self.marker:
|
||||
yield f"; {self.marker}"
|
||||
|
||||
def __getstate__(self) -> str:
|
||||
# Return the requirement string for compactness and stability.
|
||||
# Re-parsed on load to reconstruct all fields.
|
||||
return str(self)
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, str):
|
||||
# New format (26.2+): just the requirement string.
|
||||
try:
|
||||
tmp = Requirement(state)
|
||||
except InvalidRequirement as exc:
|
||||
raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
|
||||
self.name = tmp.name
|
||||
self.url = tmp.url
|
||||
self.extras = tmp.extras
|
||||
self.specifier = tmp.specifier
|
||||
self.marker = tmp.marker
|
||||
return
|
||||
if isinstance(state, dict):
|
||||
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||
self.__dict__.update(state)
|
||||
return
|
||||
raise TypeError(f"Cannot restore Requirement from {state!r}")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "".join(self._iter_parts(self.name))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}('{self}')>"
|
||||
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(tuple(self._iter_parts(canonicalize_name(self.name))))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import platform
|
||||
import re
|
||||
import struct
|
||||
|
|
@ -13,20 +14,52 @@ import sys
|
|||
import sysconfig
|
||||
from importlib.machinery import EXTENSION_SUFFIXES
|
||||
from typing import (
|
||||
Any,
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
from . import _manylinux, _musllinux
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import AbstractSet
|
||||
|
||||
|
||||
__all__ = [
|
||||
"INTERPRETER_SHORT_NAMES",
|
||||
"AppleVersion",
|
||||
"PythonVersion",
|
||||
"Tag",
|
||||
"UnsortedTagsError",
|
||||
"android_platforms",
|
||||
"compatible_tags",
|
||||
"cpython_tags",
|
||||
"create_compatible_tags_selector",
|
||||
"generic_tags",
|
||||
"interpreter_name",
|
||||
"interpreter_version",
|
||||
"ios_platforms",
|
||||
"mac_platforms",
|
||||
"parse_tag",
|
||||
"platform_tags",
|
||||
"sys_tags",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PythonVersion = Sequence[int]
|
||||
AppleVersion = Tuple[int, int]
|
||||
_T = TypeVar("_T")
|
||||
|
||||
INTERPRETER_SHORT_NAMES: dict[str, str] = {
|
||||
"python": "py", # Generic.
|
||||
|
|
@ -37,7 +70,19 @@ INTERPRETER_SHORT_NAMES: dict[str, str] = {
|
|||
}
|
||||
|
||||
|
||||
_32_BIT_INTERPRETER = struct.calcsize("P") == 4
|
||||
# This function can be unit tested without reloading the module
|
||||
# (Unlike _32_BIT_INTERPRETER)
|
||||
def _compute_32_bit_interpreter() -> bool:
|
||||
return struct.calcsize("P") == 4
|
||||
|
||||
|
||||
_32_BIT_INTERPRETER = _compute_32_bit_interpreter()
|
||||
|
||||
|
||||
class UnsortedTagsError(ValueError):
|
||||
"""
|
||||
Raised when a tag component is not in sorted order per PEP 425.
|
||||
"""
|
||||
|
||||
|
||||
class Tag:
|
||||
|
|
@ -46,11 +91,29 @@ class Tag:
|
|||
|
||||
Instances are considered immutable and thus are hashable. Equality checking
|
||||
is also supported.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
.. versionchanged:: 26.2
|
||||
|
||||
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
"""
|
||||
|
||||
__slots__ = ["_abi", "_hash", "_interpreter", "_platform"]
|
||||
|
||||
def __init__(self, interpreter: str, abi: str, platform: str) -> None:
|
||||
"""
|
||||
:param str interpreter: The interpreter name, e.g. ``"py"``
|
||||
(see :attr:`INTERPRETER_SHORT_NAMES` for mapping
|
||||
well-known interpreter names to their short names).
|
||||
:param str abi: The ABI that a wheel supports, e.g. ``"cp37m"``.
|
||||
:param str platform: The OS/platform the wheel supports,
|
||||
e.g. ``"win_amd64"``.
|
||||
"""
|
||||
self._interpreter = interpreter.lower()
|
||||
self._abi = abi.lower()
|
||||
self._platform = platform.lower()
|
||||
|
|
@ -63,14 +126,25 @@ class Tag:
|
|||
|
||||
@property
|
||||
def interpreter(self) -> str:
|
||||
"""
|
||||
The interpreter name, e.g. ``"py"`` (see
|
||||
:attr:`INTERPRETER_SHORT_NAMES` for mapping well-known interpreter
|
||||
names to their short names).
|
||||
"""
|
||||
return self._interpreter
|
||||
|
||||
@property
|
||||
def abi(self) -> str:
|
||||
"""
|
||||
The supported ABI.
|
||||
"""
|
||||
return self._abi
|
||||
|
||||
@property
|
||||
def platform(self) -> str:
|
||||
"""
|
||||
The OS/platform.
|
||||
"""
|
||||
return self._platform
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
|
|
@ -93,23 +167,69 @@ class Tag:
|
|||
def __repr__(self) -> str:
|
||||
return f"<{self} @ {id(self)}>"
|
||||
|
||||
def __setstate__(self, state: tuple[None, dict[str, Any]]) -> None:
|
||||
# The cached _hash is wrong when unpickling.
|
||||
_, slots = state
|
||||
for k, v in slots.items():
|
||||
setattr(self, k, v)
|
||||
self._hash = hash((self._interpreter, self._abi, self._platform))
|
||||
def __getstate__(self) -> tuple[str, str, str]:
|
||||
# Return state as a 3-item tuple: (interpreter, abi, platform).
|
||||
# Cache member _hash is excluded and will be recomputed.
|
||||
return (self._interpreter, self._abi, self._platform)
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, tuple):
|
||||
if len(state) == 3 and all(isinstance(s, str) for s in state):
|
||||
# New format (26.2+): (interpreter, abi, platform)
|
||||
self._interpreter, self._abi, self._platform = state
|
||||
self._hash = hash((self._interpreter, self._abi, self._platform))
|
||||
return
|
||||
if len(state) == 2 and isinstance(state[1], dict):
|
||||
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
|
||||
_, slots = state
|
||||
try:
|
||||
interpreter = slots["_interpreter"]
|
||||
abi = slots["_abi"]
|
||||
platform = slots["_platform"]
|
||||
except KeyError:
|
||||
raise TypeError(f"Cannot restore Tag from {state!r}") from None
|
||||
if not all(
|
||||
isinstance(value, str) for value in (interpreter, abi, platform)
|
||||
):
|
||||
raise TypeError(f"Cannot restore Tag from {state!r}")
|
||||
self._interpreter = interpreter.lower()
|
||||
self._abi = abi.lower()
|
||||
self._platform = platform.lower()
|
||||
self._hash = hash((self._interpreter, self._abi, self._platform))
|
||||
return
|
||||
raise TypeError(f"Cannot restore Tag from {state!r}")
|
||||
|
||||
|
||||
def parse_tag(tag: str) -> frozenset[Tag]:
|
||||
def parse_tag(tag: str, *, validate_order: bool = False) -> frozenset[Tag]:
|
||||
"""
|
||||
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
|
||||
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of
|
||||
:class:`Tag` instances.
|
||||
|
||||
Returning a set is required due to the possibility that the tag is a
|
||||
compressed tag set.
|
||||
`compressed tag set`_, e.g. ``"py2.py3-none-any"`` which supports both
|
||||
Python 2 and Python 3.
|
||||
|
||||
If **validate_order** is true, compressed tag set components are checked
|
||||
to be in sorted order as required by PEP 425.
|
||||
|
||||
:param str tag: The tag to parse, e.g. ``"py3-none-any"``.
|
||||
:param bool validate_order: Check whether compressed tag set components
|
||||
are in sorted order.
|
||||
:raises UnsortedTagsError: If **validate_order** is true and any compressed tag
|
||||
set component is not in sorted order.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
The *validate_order* parameter.
|
||||
"""
|
||||
tags = set()
|
||||
interpreters, abis, platforms = tag.split("-")
|
||||
if validate_order:
|
||||
for component in (interpreters, abis, platforms):
|
||||
parts = component.split(".")
|
||||
if parts != sorted(parts):
|
||||
raise UnsortedTagsError(
|
||||
f"Tag component {component!r} is not in sorted order per PEP 425"
|
||||
)
|
||||
for interpreter in interpreters.split("."):
|
||||
for abi in abis.split("."):
|
||||
for platform_ in platforms.split("."):
|
||||
|
|
@ -150,12 +270,25 @@ def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
|
|||
"""
|
||||
Determine if the Python version supports abi3.
|
||||
|
||||
PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`)
|
||||
PEP 384 was first implemented in Python 3.2. The free-threaded
|
||||
builds do not support abi3.
|
||||
"""
|
||||
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
|
||||
|
||||
|
||||
def _abi3t_applies(python_version: PythonVersion, threading: bool) -> bool:
|
||||
"""
|
||||
Determine if the Python version supports abi3t.
|
||||
|
||||
PEP 803 was first implemented in Python 3.15 but, per PEP 803, this
|
||||
returns tags going back to Python 3.2 to mirror the abi3
|
||||
implementation and leave open the possibility of abi3t wheels
|
||||
supporting older Python versions.
|
||||
|
||||
"""
|
||||
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and threading
|
||||
|
||||
|
||||
def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]:
|
||||
py_version = tuple(py_version) # To allow for version comparison.
|
||||
abis = []
|
||||
|
|
@ -197,19 +330,31 @@ def cpython_tags(
|
|||
warn: bool = False,
|
||||
) -> Iterator[Tag]:
|
||||
"""
|
||||
Yields the tags for a CPython interpreter.
|
||||
Yields the tags for the CPython interpreter.
|
||||
|
||||
The tags consist of:
|
||||
- cp<python_version>-<abi>-<platform>
|
||||
- cp<python_version>-abi3-<platform>
|
||||
- cp<python_version>-none-<platform>
|
||||
- cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2.
|
||||
The specific tags generated are:
|
||||
|
||||
If python_version only specifies a major version then user-provided ABIs and
|
||||
the 'none' ABItag will be used.
|
||||
- ``cp<python_version>-<abi>-<platform>``
|
||||
- ``cp<python_version>-<stable_abi>-<platform>``
|
||||
- ``cp<python_version>-none-<platform>``
|
||||
- ``cp<older version>-<stable_abi>-<platform>`` where "older version" is all older
|
||||
minor versions down to Python 3.2 (when ``abi3`` was introduced)
|
||||
|
||||
If 'abi3' or 'none' are specified in 'abis' then they will be yielded at
|
||||
their normal position and not at the beginning.
|
||||
If ``python_version`` only provides a major-only version then only
|
||||
user-provided ABIs via ``abis`` and the ``none`` ABI will be used.
|
||||
|
||||
The ``stable_abi`` will be either ``abi3`` or ``abi3t`` if `abi` is a
|
||||
GIL-enabled ABI like `"cp315"` or a free-threaded ABI like `"cp315t"`,
|
||||
respectively.
|
||||
|
||||
:param Sequence python_version: A one- or two-item sequence representing the
|
||||
targeted Python version. Defaults to
|
||||
``sys.version_info[:2]``.
|
||||
:param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
|
||||
compatible with the current system.
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
"""
|
||||
if not python_version:
|
||||
python_version = sys.version_info[:2]
|
||||
|
|
@ -233,16 +378,27 @@ def cpython_tags(
|
|||
|
||||
threading = _is_threaded_cpython(abis)
|
||||
use_abi3 = _abi3_applies(python_version, threading)
|
||||
if use_abi3:
|
||||
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
|
||||
yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
|
||||
use_abi3t = _abi3t_applies(python_version, threading)
|
||||
|
||||
if use_abi3:
|
||||
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
|
||||
if use_abi3t:
|
||||
yield from (Tag(interpreter, "abi3t", platform_) for platform_ in platforms)
|
||||
|
||||
yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
|
||||
|
||||
if use_abi3 or use_abi3t:
|
||||
for minor_version in range(python_version[1] - 1, 1, -1):
|
||||
for platform_ in platforms:
|
||||
version = _version_nodot((python_version[0], minor_version))
|
||||
interpreter = f"cp{version}"
|
||||
yield Tag(interpreter, "abi3", platform_)
|
||||
if use_abi3:
|
||||
yield Tag(interpreter, "abi3", platform_)
|
||||
if use_abi3t:
|
||||
# Support for abi3t was introduced in Python 3.15, but in
|
||||
# principle abi3t wheels are possible for older limited API
|
||||
# versions, so allow things like ("cp37", "abi3t", "platform")
|
||||
yield Tag(interpreter, "abi3t", platform_)
|
||||
|
||||
|
||||
def _generic_abi() -> list[str]:
|
||||
|
|
@ -294,12 +450,25 @@ def generic_tags(
|
|||
warn: bool = False,
|
||||
) -> Iterator[Tag]:
|
||||
"""
|
||||
Yields the tags for a generic interpreter.
|
||||
Yields the tags for an interpreter which requires no specialization.
|
||||
|
||||
The tags consist of:
|
||||
- <interpreter>-<abi>-<platform>
|
||||
This function should be used if one of the other interpreter-specific
|
||||
functions provided by this module is not appropriate (i.e. not calculating
|
||||
tags for a CPython interpreter).
|
||||
|
||||
The "none" ABI will be added if it was not explicitly provided.
|
||||
The specific tags generated are:
|
||||
|
||||
- ``<interpreter>-<abi>-<platform>``
|
||||
|
||||
The ``"none"`` ABI will be added if it was not explicitly provided.
|
||||
|
||||
:param str interpreter: The name of the interpreter. Defaults to being
|
||||
calculated.
|
||||
:param Iterable abis: Iterable of compatible ABIs. Defaults to the ABIs
|
||||
compatible with the current system.
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
"""
|
||||
if not interpreter:
|
||||
interp_name = interpreter_name()
|
||||
|
|
@ -335,12 +504,22 @@ def compatible_tags(
|
|||
platforms: Iterable[str] | None = None,
|
||||
) -> Iterator[Tag]:
|
||||
"""
|
||||
Yields the sequence of tags that are compatible with a specific version of Python.
|
||||
Yields the tags for an interpreter compatible with the Python version
|
||||
specified by ``python_version``.
|
||||
|
||||
The tags consist of:
|
||||
- py*-none-<platform>
|
||||
- <interpreter>-none-any # ... if `interpreter` is provided.
|
||||
- py*-none-any
|
||||
The specific tags generated are:
|
||||
|
||||
- ``py*-none-<platform>``
|
||||
- ``<interpreter>-none-any`` if ``interpreter`` is provided
|
||||
- ``py*-none-any``
|
||||
|
||||
:param Sequence python_version: A one- or two-item sequence representing the
|
||||
compatible version of Python. Defaults to
|
||||
``sys.version_info[:2]``.
|
||||
:param str interpreter: The name of the interpreter (if known), e.g.
|
||||
``"cp38"``. Defaults to the current interpreter.
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
"""
|
||||
if not python_version:
|
||||
python_version = sys.version_info[:2]
|
||||
|
|
@ -400,12 +579,25 @@ def mac_platforms(
|
|||
version: AppleVersion | None = None, arch: str | None = None
|
||||
) -> Iterator[str]:
|
||||
"""
|
||||
Yields the platform tags for a macOS system.
|
||||
Yields the :attr:`~Tag.platform` tags for macOS.
|
||||
|
||||
The `version` parameter is a two-item tuple specifying the macOS version to
|
||||
generate platform tags for. The `arch` parameter is the CPU architecture to
|
||||
generate platform tags for. Both parameters default to the appropriate value
|
||||
for the current system.
|
||||
|
||||
:param tuple version: A two-item tuple representing the version of macOS.
|
||||
Defaults to the current system's version.
|
||||
:param str arch: The CPU architecture. Defaults to the architecture of the
|
||||
current system, e.g. ``"x86_64"``.
|
||||
|
||||
.. note::
|
||||
Equivalent support for the other major platforms is purposefully not
|
||||
provided:
|
||||
|
||||
- On Windows, platform compatibility is statically specified
|
||||
- On Linux, code must be run on the system itself to determine
|
||||
compatibility
|
||||
"""
|
||||
version_str, _, cpu_arch = platform.mac_ver()
|
||||
if version is None:
|
||||
|
|
@ -476,14 +668,19 @@ def ios_platforms(
|
|||
version: AppleVersion | None = None, multiarch: str | None = None
|
||||
) -> Iterator[str]:
|
||||
"""
|
||||
Yields the platform tags for an iOS system.
|
||||
|
||||
:param version: A two-item tuple specifying the iOS version to generate
|
||||
platform tags for. Defaults to the current iOS version.
|
||||
:param multiarch: The CPU architecture+ABI to generate platform tags for -
|
||||
(the value used by `sys.implementation._multiarch` e.g.,
|
||||
`arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current
|
||||
multiarch value.
|
||||
Yields the :attr:`~Tag.platform` tags for iOS.
|
||||
|
||||
:param tuple version: A two-item tuple representing the version of iOS.
|
||||
Defaults to the current system's version.
|
||||
:param str multiarch: The CPU architecture+ABI to be used. This should be in
|
||||
the format by ``sys.implementation._multiarch`` (e.g.,
|
||||
``arm64_iphoneos`` or ``x86_64_iphonesimulator``).
|
||||
Defaults to the current system's multiarch value.
|
||||
|
||||
.. note::
|
||||
Behavior of this method is undefined if invoked on non-iOS platforms
|
||||
without providing explicit version and multiarch arguments.
|
||||
"""
|
||||
if version is None:
|
||||
# if iOS is the current platform, ios_ver *must* be defined. However,
|
||||
|
|
@ -585,13 +782,22 @@ def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
|
|||
yield f"linux_{arch}"
|
||||
|
||||
|
||||
def _emscripten_platforms() -> Iterator[str]:
|
||||
pyemscripten_platform_version = sysconfig.get_config_var(
|
||||
"PYEMSCRIPTEN_PLATFORM_VERSION"
|
||||
)
|
||||
if pyemscripten_platform_version:
|
||||
yield f"pyemscripten_{pyemscripten_platform_version}_wasm32"
|
||||
yield from _generic_platforms()
|
||||
|
||||
|
||||
def _generic_platforms() -> Iterator[str]:
|
||||
yield _normalize_string(sysconfig.get_platform())
|
||||
|
||||
|
||||
def platform_tags() -> Iterator[str]:
|
||||
"""
|
||||
Provides the platform tags for this installation.
|
||||
Yields the :attr:`~Tag.platform` tags for the running interpreter.
|
||||
"""
|
||||
if platform.system() == "Darwin":
|
||||
return mac_platforms()
|
||||
|
|
@ -601,6 +807,8 @@ def platform_tags() -> Iterator[str]:
|
|||
return android_platforms()
|
||||
elif platform.system() == "Linux":
|
||||
return _linux_platforms()
|
||||
elif platform.system() == "Emscripten":
|
||||
return _emscripten_platforms()
|
||||
else:
|
||||
return _generic_platforms()
|
||||
|
||||
|
|
@ -611,6 +819,8 @@ def interpreter_name() -> str:
|
|||
|
||||
Some implementations have a reserved, two-letter abbreviation which will
|
||||
be returned when appropriate.
|
||||
|
||||
This typically acts as the prefix to the :attr:`~Tag.interpreter` tag.
|
||||
"""
|
||||
name = sys.implementation.name
|
||||
return INTERPRETER_SHORT_NAMES.get(name) or name
|
||||
|
|
@ -618,7 +828,11 @@ def interpreter_name() -> str:
|
|||
|
||||
def interpreter_version(*, warn: bool = False) -> str:
|
||||
"""
|
||||
Returns the version of the running interpreter.
|
||||
Returns the running interpreter's version.
|
||||
|
||||
This typically acts as the suffix to the :attr:`~Tag.interpreter` tag.
|
||||
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
"""
|
||||
version = _get_config_var("py_version_nodot", warn=warn)
|
||||
return str(version) if version else _version_nodot(sys.version_info[:2])
|
||||
|
|
@ -630,10 +844,31 @@ def _version_nodot(version: PythonVersion) -> str:
|
|||
|
||||
def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
|
||||
"""
|
||||
Returns the sequence of tag triples for the running interpreter.
|
||||
Yields the sequence of tag triples that the running interpreter supports.
|
||||
|
||||
The order of the sequence corresponds to priority order for the
|
||||
interpreter, from most to least important.
|
||||
The iterable is ordered so that the best-matching tag is first in the
|
||||
sequence. The exact preferential order to tags is interpreter-specific, but
|
||||
in general the tag importance is in the order of:
|
||||
|
||||
1. Interpreter
|
||||
2. Platform
|
||||
3. ABI
|
||||
|
||||
This order is due to the fact that an ABI is inherently tied to the
|
||||
platform, but platform-specific code is not necessarily tied to the ABI. The
|
||||
interpreter is the most important tag as it dictates basic support for any
|
||||
wheel.
|
||||
|
||||
The function returns an iterable in order to allow for the possible
|
||||
short-circuiting of tag generation if the entire sequence is not necessary
|
||||
and tag calculation happens to be expensive.
|
||||
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
|
||||
.. versionchanged:: 21.3
|
||||
Added the `pp3-none-any` tag (:issue:`311`).
|
||||
.. versionchanged:: 27.0
|
||||
Added the `abi3t` tag (:issue:`1099`).
|
||||
"""
|
||||
|
||||
interp_name = interpreter_name()
|
||||
|
|
@ -649,3 +884,49 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
|
|||
else:
|
||||
interp = None
|
||||
yield from compatible_tags(interpreter=interp)
|
||||
|
||||
|
||||
def create_compatible_tags_selector(
|
||||
tags: Iterable[Tag],
|
||||
) -> Callable[[Iterable[tuple[_T, AbstractSet[Tag]]]], Iterator[_T]]:
|
||||
"""Create a callable to select things compatible with supported tags.
|
||||
|
||||
This function accepts an ordered sequence of tags, with the preferred
|
||||
tags first.
|
||||
|
||||
The returned callable accepts an iterable of tuples (thing, set[Tag]),
|
||||
and returns an iterator of things, with the things with the best
|
||||
matching tags first.
|
||||
|
||||
Example to select compatible wheel filenames:
|
||||
|
||||
>>> from packaging import tags
|
||||
>>> from packaging.utils import parse_wheel_filename
|
||||
>>> selector = tags.create_compatible_tags_selector(tags.sys_tags())
|
||||
>>> filenames = ["foo-1.0-py3-none-any.whl", "foo-1.0-py2-none-any.whl"]
|
||||
>>> list(selector([
|
||||
... (filename, parse_wheel_filename(filename)[-1]) for filename in filenames
|
||||
... ]))
|
||||
['foo-1.0-py3-none-any.whl']
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
tag_ranks: dict[Tag, int] = {}
|
||||
for rank, tag in enumerate(tags):
|
||||
tag_ranks.setdefault(tag, rank) # ignore duplicate tags, keep first
|
||||
supported_tags = tag_ranks.keys()
|
||||
|
||||
def selector(
|
||||
tagged_things: Iterable[tuple[_T, AbstractSet[Tag]]],
|
||||
) -> Iterator[_T]:
|
||||
ranked_things: list[tuple[_T, int]] = []
|
||||
for thing, thing_tags in tagged_things:
|
||||
supported_thing_tags = thing_tags & supported_tags
|
||||
if supported_thing_tags:
|
||||
thing_rank = min(tag_ranks[t] for t in supported_thing_tags)
|
||||
ranked_things.append((thing, thing_rank))
|
||||
return iter(
|
||||
thing for thing, _ in sorted(ranked_things, key=operator.itemgetter(1))
|
||||
)
|
||||
|
||||
return selector
|
||||
|
|
|
|||
|
|
@ -7,11 +7,33 @@ from __future__ import annotations
|
|||
import re
|
||||
from typing import NewType, Tuple, Union, cast
|
||||
|
||||
from .tags import Tag, parse_tag
|
||||
from .tags import Tag, UnsortedTagsError, parse_tag
|
||||
from .version import InvalidVersion, Version, _TrimmedRelease
|
||||
|
||||
__all__ = [
|
||||
"BuildTag",
|
||||
"InvalidName",
|
||||
"InvalidSdistFilename",
|
||||
"InvalidWheelFilename",
|
||||
"NormalizedName",
|
||||
"canonicalize_name",
|
||||
"canonicalize_version",
|
||||
"is_normalized_name",
|
||||
"parse_sdist_filename",
|
||||
"parse_wheel_filename",
|
||||
]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
BuildTag = Union[Tuple[()], Tuple[int, str]]
|
||||
|
||||
NormalizedName = NewType("NormalizedName", str)
|
||||
"""
|
||||
A :class:`typing.NewType` of :class:`str`, representing a normalized name.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidName(ValueError):
|
||||
|
|
@ -33,13 +55,39 @@ class InvalidSdistFilename(ValueError):
|
|||
|
||||
|
||||
# Core metadata spec for `Name`
|
||||
_validate_regex = re.compile(r"[A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9]", re.IGNORECASE)
|
||||
_normalized_regex = re.compile(r"[a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9]")
|
||||
_validate_regex = re.compile(
|
||||
r"[a-z0-9]|[a-z0-9][a-z0-9._-]*[a-z0-9]", re.IGNORECASE | re.ASCII
|
||||
)
|
||||
_normalized_regex = re.compile(r"[a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9]", re.ASCII)
|
||||
# PEP 427: The build number must start with a digit.
|
||||
_build_tag_regex = re.compile(r"(\d+)(.*)")
|
||||
_build_tag_regex = re.compile(r"(\d+)(.*)", re.ASCII)
|
||||
|
||||
|
||||
def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
|
||||
"""
|
||||
This function takes a valid Python package or extra name, and returns the
|
||||
normalized form of it.
|
||||
|
||||
The return type is typed as :class:`NormalizedName`. This allows type
|
||||
checkers to help require that a string has passed through this function
|
||||
before use.
|
||||
|
||||
If **validate** is true, then the function will check if **name** is a valid
|
||||
distribution name before normalizing.
|
||||
|
||||
:param str name: The name to normalize.
|
||||
:param bool validate: Check whether the name is a valid distribution name.
|
||||
:raises InvalidName: If **validate** is true and the name is not an
|
||||
acceptable distribution name.
|
||||
|
||||
>>> from packaging.utils import canonicalize_name
|
||||
>>> canonicalize_name("Django")
|
||||
'django'
|
||||
>>> canonicalize_name("oslo.concurrency")
|
||||
'oslo-concurrency'
|
||||
>>> canonicalize_name("requests")
|
||||
'requests'
|
||||
"""
|
||||
if validate and not _validate_regex.fullmatch(name):
|
||||
raise InvalidName(f"name is invalid: {name!r}")
|
||||
# Ensure all ``.`` and ``_`` are ``-``
|
||||
|
|
@ -53,15 +101,32 @@ def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
|
|||
|
||||
|
||||
def is_normalized_name(name: str) -> bool:
|
||||
"""
|
||||
Check if a name is already normalized (i.e. :func:`canonicalize_name` would
|
||||
roundtrip to the same value).
|
||||
|
||||
:param str name: The name to check.
|
||||
|
||||
>>> from packaging.utils import is_normalized_name
|
||||
>>> is_normalized_name("requests")
|
||||
True
|
||||
>>> is_normalized_name("Django")
|
||||
False
|
||||
"""
|
||||
return _normalized_regex.fullmatch(name) is not None
|
||||
|
||||
|
||||
def canonicalize_version(
|
||||
version: Version | str, *, strip_trailing_zero: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Return a canonical form of a version as a string.
|
||||
"""Return a canonical form of a version as a string.
|
||||
|
||||
This function takes a string representing a package version (or a
|
||||
:class:`~packaging.version.Version` instance), and returns the
|
||||
normalized form of it. By default, it strips trailing zeros from
|
||||
the release segment.
|
||||
|
||||
>>> from packaging.utils import canonicalize_version
|
||||
>>> canonicalize_version('1.0.1')
|
||||
'1.0.1'
|
||||
|
||||
|
|
@ -77,6 +142,9 @@ def canonicalize_version(
|
|||
|
||||
>>> canonicalize_version('foo bar baz')
|
||||
'foo bar baz'
|
||||
|
||||
>>> canonicalize_version('1.4.0.0.0')
|
||||
'1.4'
|
||||
"""
|
||||
if isinstance(version, str):
|
||||
try:
|
||||
|
|
@ -88,7 +156,49 @@ def canonicalize_version(
|
|||
|
||||
def parse_wheel_filename(
|
||||
filename: str,
|
||||
*,
|
||||
validate_order: bool = False,
|
||||
) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
|
||||
"""
|
||||
This function takes the filename of a wheel file, and parses it,
|
||||
returning a tuple of name, version, build number, and tags.
|
||||
|
||||
The name part of the tuple is normalized and typed as
|
||||
:class:`NormalizedName`. The version portion is an instance of
|
||||
:class:`~packaging.version.Version`. The build number is ``()`` if
|
||||
there is no build number in the wheel filename, otherwise a
|
||||
two-item tuple of an integer for the leading digits and
|
||||
a string for the rest of the build number. The tags portion is a
|
||||
frozen set of :class:`~packaging.tags.Tag` instances (as the tag
|
||||
string format allows multiple tags to be combined into a single
|
||||
string).
|
||||
|
||||
If **validate_order** is true, compressed tag set components are
|
||||
checked to be in sorted order as required by PEP 425.
|
||||
|
||||
:param str filename: The name of the wheel file.
|
||||
:param bool validate_order: Check whether compressed tag set components
|
||||
are in sorted order.
|
||||
:raises InvalidWheelFilename: If the filename in question
|
||||
does not follow the :ref:`wheel specification
|
||||
<pypug:binary-distribution-format>`.
|
||||
|
||||
>>> from packaging.utils import parse_wheel_filename
|
||||
>>> from packaging.tags import Tag
|
||||
>>> from packaging.version import Version
|
||||
>>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl")
|
||||
>>> name
|
||||
'foo'
|
||||
>>> ver == Version('1.0')
|
||||
True
|
||||
>>> tags == {Tag("py3", "none", "any")}
|
||||
True
|
||||
>>> not build
|
||||
True
|
||||
|
||||
.. versionadded:: 26.1
|
||||
The *validate_order* parameter.
|
||||
"""
|
||||
if not filename.endswith(".whl"):
|
||||
raise InvalidWheelFilename(
|
||||
f"Invalid wheel filename (extension must be '.whl'): {filename!r}"
|
||||
|
|
@ -125,11 +235,39 @@ def parse_wheel_filename(
|
|||
build = cast("BuildTag", (int(build_match.group(1)), build_match.group(2)))
|
||||
else:
|
||||
build = ()
|
||||
tags = parse_tag(parts[-1])
|
||||
tag_str = parts[-1]
|
||||
try:
|
||||
tags = parse_tag(tag_str, validate_order=validate_order)
|
||||
except UnsortedTagsError:
|
||||
raise InvalidWheelFilename(
|
||||
f"Invalid wheel filename (compressed tag set components must be in "
|
||||
f"sorted order per PEP 425): {filename!r}"
|
||||
) from None
|
||||
return (name, version, build, tags)
|
||||
|
||||
|
||||
def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
|
||||
"""
|
||||
This function takes the filename of a sdist file (as specified
|
||||
in the `Source distribution format`_ documentation), and parses
|
||||
it, returning a tuple of the normalized name and version as
|
||||
represented by an instance of :class:`~packaging.version.Version`.
|
||||
|
||||
:param str filename: The name of the sdist file.
|
||||
:raises InvalidSdistFilename: If the filename does not end
|
||||
with an sdist extension (``.zip`` or ``.tar.gz``), or if it does not
|
||||
contain a dash separating the name and the version of the distribution.
|
||||
|
||||
>>> from packaging.utils import parse_sdist_filename
|
||||
>>> from packaging.version import Version
|
||||
>>> name, ver = parse_sdist_filename("foo-1.0.tar.gz")
|
||||
>>> name
|
||||
'foo'
|
||||
>>> ver == Version('1.0')
|
||||
True
|
||||
|
||||
.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
|
||||
"""
|
||||
if filename.endswith(".tar.gz"):
|
||||
file_stem = filename[: -len(".tar.gz")]
|
||||
elif filename.endswith(".zip"):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"""
|
||||
.. testsetup::
|
||||
|
||||
from packaging.version import parse, Version
|
||||
from packaging.version import parse, normalize_pre, Version, _cmpkey
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -23,8 +23,6 @@ from typing import (
|
|||
Union,
|
||||
)
|
||||
|
||||
from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing_extensions import Self, Unpack
|
||||
|
||||
|
|
@ -37,7 +35,7 @@ else: # pragma: no cover
|
|||
import warnings
|
||||
|
||||
def _deprecated(message: str) -> object:
|
||||
def decorator(func: object) -> object:
|
||||
def decorator(func: Callable[[...], object]) -> object:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: object, **kwargs: object) -> object:
|
||||
warnings.warn(
|
||||
|
|
@ -62,22 +60,20 @@ _LETTER_NORMALIZATION = {
|
|||
"r": "post",
|
||||
}
|
||||
|
||||
__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"]
|
||||
__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "normalize_pre", "parse"]
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
LocalType = Tuple[Union[int, str], ...]
|
||||
|
||||
CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]]
|
||||
CmpLocalType = Union[
|
||||
NegativeInfinityType,
|
||||
Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...],
|
||||
]
|
||||
CmpKey = Tuple[
|
||||
int,
|
||||
Tuple[int, ...],
|
||||
CmpPrePostDevType,
|
||||
CmpPrePostDevType,
|
||||
CmpPrePostDevType,
|
||||
CmpLocalType,
|
||||
CmpLocalType = Tuple[Tuple[int, str], ...]
|
||||
CmpSuffix = Tuple[int, int, int, int, int, int]
|
||||
CmpKey = Union[
|
||||
Tuple[int, Tuple[int, ...], CmpSuffix],
|
||||
Tuple[int, Tuple[int, ...], CmpSuffix, CmpLocalType],
|
||||
]
|
||||
VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
|
||||
|
||||
|
|
@ -85,15 +81,38 @@ VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
|
|||
class _VersionReplace(TypedDict, total=False):
|
||||
epoch: int | None
|
||||
release: tuple[int, ...] | None
|
||||
pre: tuple[Literal["a", "b", "rc"], int] | None
|
||||
pre: tuple[str, int] | None
|
||||
post: int | None
|
||||
dev: int | None
|
||||
local: str | None
|
||||
|
||||
|
||||
def normalize_pre(letter: str, /) -> str:
|
||||
"""Normalize the pre-release segment of a version string.
|
||||
|
||||
Returns a lowercase version of the string if not a known pre-release
|
||||
identifier.
|
||||
|
||||
>>> normalize_pre('alpha')
|
||||
'a'
|
||||
>>> normalize_pre('BETA')
|
||||
'b'
|
||||
>>> normalize_pre('rc')
|
||||
'rc'
|
||||
|
||||
:param letter:
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
letter = letter.lower()
|
||||
return _LETTER_NORMALIZATION.get(letter, letter)
|
||||
|
||||
|
||||
def parse(version: str) -> Version:
|
||||
"""Parse the given version string.
|
||||
|
||||
This is identical to the :class:`Version` constructor.
|
||||
|
||||
>>> parse('1.0.dev1')
|
||||
<Version('1.0.dev1')>
|
||||
|
||||
|
|
@ -173,7 +192,7 @@ class _BaseVersion:
|
|||
# Note that ++ doesn't behave identically on CPython and PyPy, so not using it here
|
||||
_VERSION_PATTERN = r"""
|
||||
v?+ # optional leading v
|
||||
(?:
|
||||
(?a:
|
||||
(?:(?P<epoch>[0-9]+)!)?+ # epoch
|
||||
(?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment
|
||||
(?P<pre> # pre-release
|
||||
|
|
@ -199,7 +218,7 @@ _VERSION_PATTERN = r"""
|
|||
(?P<dev_n>[0-9]+)?
|
||||
)?+
|
||||
)
|
||||
(?:\+
|
||||
(?a:\+
|
||||
(?P<local> # local version
|
||||
[a-z0-9]+
|
||||
(?:[._-][a-z0-9]+)*+
|
||||
|
|
@ -227,12 +246,21 @@ expressions (for example, matching a version number as part of a file name). The
|
|||
regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
|
||||
flags set.
|
||||
|
||||
.. versionchanged:: 26.0
|
||||
|
||||
The regex now uses possessive qualifiers on Python 3.11 if they are
|
||||
supported (CPython 3.11.5+, PyPy 3.11.13+).
|
||||
|
||||
:meta hide-value:
|
||||
"""
|
||||
|
||||
|
||||
# Validation pattern for local version in replace()
|
||||
_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE)
|
||||
_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE | re.ASCII)
|
||||
|
||||
# Fast path: If a version has only digits and dots then we
|
||||
# can skip the regex and parse it as a release segment
|
||||
_SIMPLE_VERSION_INDICATORS = frozenset(".0123456789")
|
||||
|
||||
|
||||
def _validate_epoch(value: object, /) -> int:
|
||||
|
|
@ -258,14 +286,12 @@ def _validate_release(value: object, /) -> tuple[int, ...]:
|
|||
def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None:
|
||||
if value is None:
|
||||
return value
|
||||
if (
|
||||
isinstance(value, tuple)
|
||||
and len(value) == 2
|
||||
and value[0] in ("a", "b", "rc")
|
||||
and isinstance(value[1], int)
|
||||
and value[1] >= 0
|
||||
):
|
||||
return value
|
||||
if isinstance(value, tuple) and len(value) == 2:
|
||||
letter, number = value
|
||||
letter = normalize_pre(letter)
|
||||
if letter in {"a", "b", "rc"} and isinstance(number, int) and number >= 0:
|
||||
# type checkers can't infer the Literal type here on letter
|
||||
return (letter, number) # type: ignore[return-value]
|
||||
msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}"
|
||||
raise InvalidVersion(msg)
|
||||
|
||||
|
|
@ -301,9 +327,9 @@ def _validate_local(value: object, /) -> LocalType | None:
|
|||
class _Version(NamedTuple):
|
||||
epoch: int
|
||||
release: tuple[int, ...]
|
||||
dev: tuple[str, int] | None
|
||||
pre: tuple[str, int] | None
|
||||
post: tuple[str, int] | None
|
||||
dev: tuple[Literal["dev"], int] | None
|
||||
pre: tuple[Literal["a", "b", "rc"], int] | None
|
||||
post: tuple[Literal["post"], int] | None
|
||||
local: LocalType | None
|
||||
|
||||
|
||||
|
|
@ -329,20 +355,50 @@ class Version(_BaseVersion):
|
|||
False
|
||||
>>> v1 <= v2
|
||||
True
|
||||
|
||||
:class:`Version` is immutable; use :meth:`__replace__` to change
|
||||
part of a version.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
.. versionchanged:: 26.2
|
||||
|
||||
Added a stable pickle format. Pickles created with packaging 26.2+ can
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
"""
|
||||
|
||||
__slots__ = ("_dev", "_epoch", "_key_cache", "_local", "_post", "_pre", "_release")
|
||||
__slots__ = (
|
||||
"_dev",
|
||||
"_epoch",
|
||||
"_hash_cache",
|
||||
"_key_cache",
|
||||
"_local",
|
||||
"_post",
|
||||
"_pre",
|
||||
"_release",
|
||||
)
|
||||
__match_args__ = ("_str",)
|
||||
"""
|
||||
Pattern matching is supported on Python 3.10+.
|
||||
|
||||
.. versionadded:: 26.0
|
||||
|
||||
:meta hide-value:
|
||||
"""
|
||||
|
||||
_regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE)
|
||||
|
||||
_epoch: int
|
||||
_release: tuple[int, ...]
|
||||
_dev: tuple[str, int] | None
|
||||
_pre: tuple[str, int] | None
|
||||
_post: tuple[str, int] | None
|
||||
_dev: tuple[Literal["dev"], int] | None
|
||||
_pre: tuple[Literal["a", "b", "rc"], int] | None
|
||||
_post: tuple[Literal["post"], int] | None
|
||||
_local: LocalType | None
|
||||
|
||||
_hash_cache: int | None
|
||||
_key_cache: CmpKey | None
|
||||
|
||||
def __init__(self, version: str) -> None:
|
||||
|
|
@ -355,23 +411,118 @@ class Version(_BaseVersion):
|
|||
If the ``version`` does not conform to PEP 440 in any way then this
|
||||
exception will be raised.
|
||||
"""
|
||||
if _SIMPLE_VERSION_INDICATORS.issuperset(version):
|
||||
try:
|
||||
self._release = tuple(map(int, version.split(".")))
|
||||
except ValueError:
|
||||
# Empty parts (from "1..2", ".1", etc.) are invalid versions.
|
||||
# Any other ValueError (e.g. int str-digits limit) should
|
||||
# propagate to the caller.
|
||||
if "" in version.split("."):
|
||||
raise InvalidVersion(f"Invalid version: {version!r}") from None
|
||||
# TODO: remove "no cover" when Python 3.9 is dropped.
|
||||
raise # pragma: no cover
|
||||
|
||||
self._epoch = 0
|
||||
self._pre = None
|
||||
self._post = None
|
||||
self._dev = None
|
||||
self._local = None
|
||||
self._key_cache = None
|
||||
self._hash_cache = None
|
||||
return
|
||||
|
||||
# Validate the version and parse it into pieces
|
||||
match = self._regex.fullmatch(version)
|
||||
if not match:
|
||||
raise InvalidVersion(f"Invalid version: {version!r}")
|
||||
self._epoch = int(match.group("epoch")) if match.group("epoch") else 0
|
||||
self._release = tuple(map(int, match.group("release").split(".")))
|
||||
self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n"))
|
||||
self._post = _parse_letter_version(
|
||||
# We can type ignore the assignments below because the regex guarantees
|
||||
# the correct strings
|
||||
self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n")) # type: ignore[assignment]
|
||||
self._post = _parse_letter_version( # type: ignore[assignment]
|
||||
match.group("post_l"), match.group("post_n1") or match.group("post_n2")
|
||||
)
|
||||
self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n"))
|
||||
self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n")) # type: ignore[assignment]
|
||||
self._local = _parse_local_version(match.group("local"))
|
||||
|
||||
# Key which will be used for sorting
|
||||
self._key_cache = None
|
||||
self._hash_cache = None
|
||||
|
||||
@classmethod
|
||||
def from_parts(
|
||||
cls,
|
||||
*,
|
||||
epoch: int = 0,
|
||||
release: tuple[int, ...],
|
||||
pre: tuple[str, int] | None = None,
|
||||
post: int | None = None,
|
||||
dev: int | None = None,
|
||||
local: str | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
Return a new version composed of the various parts.
|
||||
|
||||
This allows you to build a version without going though a string and
|
||||
running a regular expression. It normalizes pre-release strings. The
|
||||
``release=`` keyword argument is required.
|
||||
|
||||
>>> Version.from_parts(release=(1,2,3))
|
||||
<Version('1.2.3')>
|
||||
>>> Version.from_parts(release=(0,1,0), pre=("b", 1))
|
||||
<Version('0.1.0b1')>
|
||||
|
||||
:param epoch:
|
||||
:param release: This version tuple is required
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
_epoch = _validate_epoch(epoch)
|
||||
_release = _validate_release(release)
|
||||
_pre = _validate_pre(pre) if pre is not None else None
|
||||
_post = _validate_post(post) if post is not None else None
|
||||
_dev = _validate_dev(dev) if dev is not None else None
|
||||
_local = _validate_local(local) if local is not None else None
|
||||
|
||||
new_version = cls.__new__(cls)
|
||||
new_version._key_cache = None
|
||||
new_version._hash_cache = None
|
||||
new_version._epoch = _epoch
|
||||
new_version._release = _release
|
||||
new_version._pre = _pre
|
||||
new_version._post = _post
|
||||
new_version._dev = _dev
|
||||
new_version._local = _local
|
||||
|
||||
return new_version
|
||||
|
||||
def __replace__(self, **kwargs: Unpack[_VersionReplace]) -> Self:
|
||||
"""
|
||||
__replace__(*, epoch=..., release=..., pre=..., post=..., dev=..., local=...)
|
||||
|
||||
Return a new version with parts replaced.
|
||||
|
||||
This returns a new version (unless no parts were changed). The
|
||||
pre-release is normalized. Setting a value to ``None`` clears it.
|
||||
|
||||
>>> v = Version("1.2.3")
|
||||
>>> v.__replace__(pre=("a", 1))
|
||||
<Version('1.2.3a1')>
|
||||
|
||||
:param int | None epoch:
|
||||
:param tuple[int, ...] | None release:
|
||||
:param tuple[str, int] | None pre:
|
||||
:param int | None post:
|
||||
:param int | None dev:
|
||||
:param str | None local:
|
||||
|
||||
.. versionadded:: 26.0
|
||||
.. versionchanged:: 26.1
|
||||
|
||||
The pre-release portion is now normalized.
|
||||
"""
|
||||
epoch = _validate_epoch(kwargs["epoch"]) if "epoch" in kwargs else self._epoch
|
||||
release = (
|
||||
_validate_release(kwargs["release"])
|
||||
|
|
@ -395,6 +546,7 @@ class Version(_BaseVersion):
|
|||
|
||||
new_version = self.__class__.__new__(self.__class__)
|
||||
new_version._key_cache = None
|
||||
new_version._hash_cache = None
|
||||
new_version._epoch = epoch
|
||||
new_version._release = release
|
||||
new_version._pre = pre
|
||||
|
|
@ -417,6 +569,255 @@ class Version(_BaseVersion):
|
|||
)
|
||||
return self._key_cache
|
||||
|
||||
# __hash__ must be defined when __eq__ is overridden,
|
||||
# otherwise Python sets __hash__ to None.
|
||||
def __hash__(self) -> int:
|
||||
if (cached_hash := self._hash_cache) is not None:
|
||||
return cached_hash
|
||||
|
||||
if (key := self._key_cache) is None:
|
||||
self._key_cache = key = _cmpkey(
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
)
|
||||
self._hash_cache = cached_hash = hash(key)
|
||||
return cached_hash
|
||||
|
||||
# Override comparison methods to use direct _key_cache access
|
||||
# This is faster than property access, especially before Python 3.12
|
||||
def __lt__(self, other: _BaseVersion) -> bool:
|
||||
if isinstance(other, Version):
|
||||
if self._key_cache is None:
|
||||
self._key_cache = _cmpkey(
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
)
|
||||
if other._key_cache is None:
|
||||
other._key_cache = _cmpkey(
|
||||
other._epoch,
|
||||
other._release,
|
||||
other._pre,
|
||||
other._post,
|
||||
other._dev,
|
||||
other._local,
|
||||
)
|
||||
return self._key_cache < other._key_cache
|
||||
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return super().__lt__(other)
|
||||
|
||||
def __le__(self, other: _BaseVersion) -> bool:
|
||||
if isinstance(other, Version):
|
||||
if self._key_cache is None:
|
||||
self._key_cache = _cmpkey(
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
)
|
||||
if other._key_cache is None:
|
||||
other._key_cache = _cmpkey(
|
||||
other._epoch,
|
||||
other._release,
|
||||
other._pre,
|
||||
other._post,
|
||||
other._dev,
|
||||
other._local,
|
||||
)
|
||||
return self._key_cache <= other._key_cache
|
||||
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return super().__le__(other)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, Version):
|
||||
if self._key_cache is None:
|
||||
self._key_cache = _cmpkey(
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
)
|
||||
if other._key_cache is None:
|
||||
other._key_cache = _cmpkey(
|
||||
other._epoch,
|
||||
other._release,
|
||||
other._pre,
|
||||
other._post,
|
||||
other._dev,
|
||||
other._local,
|
||||
)
|
||||
return self._key_cache == other._key_cache
|
||||
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return super().__eq__(other)
|
||||
|
||||
def __ge__(self, other: _BaseVersion) -> bool:
|
||||
if isinstance(other, Version):
|
||||
if self._key_cache is None:
|
||||
self._key_cache = _cmpkey(
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
)
|
||||
if other._key_cache is None:
|
||||
other._key_cache = _cmpkey(
|
||||
other._epoch,
|
||||
other._release,
|
||||
other._pre,
|
||||
other._post,
|
||||
other._dev,
|
||||
other._local,
|
||||
)
|
||||
return self._key_cache >= other._key_cache
|
||||
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return super().__ge__(other)
|
||||
|
||||
def __gt__(self, other: _BaseVersion) -> bool:
|
||||
if isinstance(other, Version):
|
||||
if self._key_cache is None:
|
||||
self._key_cache = _cmpkey(
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
)
|
||||
if other._key_cache is None:
|
||||
other._key_cache = _cmpkey(
|
||||
other._epoch,
|
||||
other._release,
|
||||
other._pre,
|
||||
other._post,
|
||||
other._dev,
|
||||
other._local,
|
||||
)
|
||||
return self._key_cache > other._key_cache
|
||||
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return super().__gt__(other)
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
if isinstance(other, Version):
|
||||
if self._key_cache is None:
|
||||
self._key_cache = _cmpkey(
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
)
|
||||
if other._key_cache is None:
|
||||
other._key_cache = _cmpkey(
|
||||
other._epoch,
|
||||
other._release,
|
||||
other._pre,
|
||||
other._post,
|
||||
other._dev,
|
||||
other._local,
|
||||
)
|
||||
return self._key_cache != other._key_cache
|
||||
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return super().__ne__(other)
|
||||
|
||||
def __getstate__(
|
||||
self,
|
||||
) -> tuple[
|
||||
int,
|
||||
tuple[int, ...],
|
||||
tuple[str, int] | None,
|
||||
tuple[str, int] | None,
|
||||
tuple[str, int] | None,
|
||||
LocalType | None,
|
||||
]:
|
||||
# Return state as a 6-item tuple for compactness:
|
||||
# (epoch, release, pre, post, dev, local)
|
||||
# Cache members are excluded and will be recomputed on demand
|
||||
return (
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
)
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
# Always discard cached values — they may contain stale references
|
||||
# (e.g. packaging._structures.InfinityType from pre-26.1 pickles)
|
||||
# and will be recomputed on demand from the core fields above.
|
||||
self._key_cache = None
|
||||
self._hash_cache = None
|
||||
|
||||
if isinstance(state, tuple):
|
||||
if len(state) == 6:
|
||||
# New format (26.2+): (epoch, release, pre, post, dev, local)
|
||||
(
|
||||
self._epoch,
|
||||
self._release,
|
||||
self._pre,
|
||||
self._post,
|
||||
self._dev,
|
||||
self._local,
|
||||
) = state
|
||||
return
|
||||
if len(state) == 2:
|
||||
# Format (packaging 26.0-26.1): (None, {slot: value}).
|
||||
_, slot_dict = state
|
||||
if isinstance(slot_dict, dict):
|
||||
self._epoch = slot_dict["_epoch"]
|
||||
self._release = slot_dict["_release"]
|
||||
self._pre = slot_dict.get("_pre")
|
||||
self._post = slot_dict.get("_post")
|
||||
self._dev = slot_dict.get("_dev")
|
||||
self._local = slot_dict.get("_local")
|
||||
return
|
||||
if isinstance(state, dict):
|
||||
# Old format (packaging <= 25.x, no __slots__): state is a plain
|
||||
# dict with "_version" (_Version NamedTuple) and "_key" entries.
|
||||
version_nt = state.get("_version")
|
||||
if version_nt is not None:
|
||||
self._epoch = version_nt.epoch
|
||||
self._release = version_nt.release
|
||||
self._pre = version_nt.pre
|
||||
self._post = version_nt.post
|
||||
self._dev = version_nt.dev
|
||||
self._local = version_nt.local
|
||||
return
|
||||
|
||||
raise TypeError(f"Cannot restore Version from {state!r}")
|
||||
|
||||
@property
|
||||
@_deprecated("Version._version is private and will be removed soon")
|
||||
def _version(self) -> _Version:
|
||||
|
|
@ -434,6 +835,7 @@ class Version(_BaseVersion):
|
|||
self._post = value.post
|
||||
self._local = value.local
|
||||
self._key_cache = None
|
||||
self._hash_cache = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""A representation of the Version that shows all internal state.
|
||||
|
|
@ -441,7 +843,7 @@ class Version(_BaseVersion):
|
|||
>>> Version('1.0.0')
|
||||
<Version('1.0.0')>
|
||||
"""
|
||||
return f"<Version('{self}')>"
|
||||
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""A string representation of the version that can be round-tripped.
|
||||
|
|
@ -507,7 +909,7 @@ class Version(_BaseVersion):
|
|||
return self._release
|
||||
|
||||
@property
|
||||
def pre(self) -> tuple[str, int] | None:
|
||||
def pre(self) -> tuple[Literal["a", "b", "rc"], int] | None:
|
||||
"""The pre-release segment of the version.
|
||||
|
||||
>>> print(Version("1.2.3").pre)
|
||||
|
|
@ -561,6 +963,9 @@ class Version(_BaseVersion):
|
|||
def public(self) -> str:
|
||||
"""The public portion of the version.
|
||||
|
||||
This returns a string. If you want a :class:`Version` again and care
|
||||
about performance, use ``v.__replace__(local=None)`` instead.
|
||||
|
||||
>>> Version("1.2.3").public
|
||||
'1.2.3'
|
||||
>>> Version("1.2.3+abc").public
|
||||
|
|
@ -574,6 +979,10 @@ class Version(_BaseVersion):
|
|||
def base_version(self) -> str:
|
||||
"""The "base version" of the version.
|
||||
|
||||
This returns a string. If you want a :class:`Version` again and care
|
||||
about performance, use
|
||||
``v.__replace__(pre=None, post=None, dev=None, local=None)`` instead.
|
||||
|
||||
>>> Version("1.2.3").base_version
|
||||
'1.2.3'
|
||||
>>> Version("1.2.3+abc").base_version
|
||||
|
|
@ -721,7 +1130,8 @@ _local_version_separators = re.compile(r"[\._-]")
|
|||
|
||||
def _parse_local_version(local: str | None) -> LocalType | None:
|
||||
"""
|
||||
Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
|
||||
Takes a string like ``"abc.1.twelve"`` and turns it into
|
||||
``("abc", 1, "twelve")``.
|
||||
"""
|
||||
if local is not None:
|
||||
return tuple(
|
||||
|
|
@ -731,6 +1141,19 @@ def _parse_local_version(local: str | None) -> LocalType | None:
|
|||
return None
|
||||
|
||||
|
||||
# Sort ranks for pre-release: dev-only < a < b < rc < stable (no pre-release).
|
||||
_PRE_RANK = {"a": 0, "b": 1, "rc": 2}
|
||||
_PRE_RANK_DEV_ONLY = -1 # sorts before a(0)
|
||||
_PRE_RANK_STABLE = 3 # sorts after rc(2)
|
||||
|
||||
# In local version segments, strings sort before ints per PEP 440.
|
||||
_LOCAL_STR_RANK = -1 # sorts before all non-negative ints
|
||||
|
||||
# Pre-computed suffix for stable releases (no pre, post, or dev segments).
|
||||
# See _cmpkey() for the suffix layout.
|
||||
_STABLE_SUFFIX = (_PRE_RANK_STABLE, 0, 0, 0, 1, 0)
|
||||
|
||||
|
||||
def _cmpkey(
|
||||
epoch: int,
|
||||
release: tuple[int, ...],
|
||||
|
|
@ -739,54 +1162,70 @@ def _cmpkey(
|
|||
dev: tuple[str, int] | None,
|
||||
local: LocalType | None,
|
||||
) -> CmpKey:
|
||||
# When we compare a release version, we want to compare it with all of the
|
||||
# trailing zeros removed. We will use this for our sorting key.
|
||||
"""Build a comparison key for PEP 440 ordering.
|
||||
|
||||
Returns ``(epoch, release, suffix)`` or
|
||||
``(epoch, release, suffix, local)`` so that plain tuple
|
||||
comparison gives the correct order.
|
||||
|
||||
Trailing zeros are stripped from the release so that ``1.0.0 == 1``.
|
||||
|
||||
The suffix is a flat 6-int tuple that encodes pre/post/dev:
|
||||
``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``
|
||||
|
||||
pre_rank: dev-only=-1, a=0, b=1, rc=2, no-pre=3
|
||||
Dev-only releases (no pre or post) get -1 so they sort before
|
||||
any alpha/beta/rc. Releases without a pre-release tag get 3
|
||||
so they sort after rc.
|
||||
post_rank: no-post=0, post=1
|
||||
Releases without a post segment sort before those with one.
|
||||
dev_rank: dev=0, no-dev=1
|
||||
Releases without a dev segment sort after those with one.
|
||||
|
||||
Local segments use ``(n, "")`` for ints and ``(-1, s)`` for strings,
|
||||
following PEP 440: strings sort before ints, strings compare
|
||||
lexicographically, ints compare numerically, and shorter segments
|
||||
sort before longer when prefixes match. Versions without a local
|
||||
segment sort before those with one (3-tuple < 4-tuple).
|
||||
|
||||
>>> _cmpkey(0, (1, 0, 0), None, None, None, None)
|
||||
(0, (1,), (3, 0, 0, 0, 1, 0))
|
||||
>>> _cmpkey(0, (1,), ("a", 1), None, None, None)
|
||||
(0, (1,), (0, 1, 0, 0, 1, 0))
|
||||
>>> _cmpkey(0, (1,), None, None, None, ("ubuntu", 1))
|
||||
(0, (1,), (3, 0, 0, 0, 1, 0), ((-1, 'ubuntu'), (1, '')))
|
||||
"""
|
||||
# Strip trailing zeros: 1.0.0 compares equal to 1.
|
||||
len_release = len(release)
|
||||
i = len_release
|
||||
while i and release[i - 1] == 0:
|
||||
i -= 1
|
||||
_release = release if i == len_release else release[:i]
|
||||
trimmed = release if i == len_release else release[:i]
|
||||
|
||||
# Fast path: stable release with no local segment.
|
||||
if pre is None and post is None and dev is None and local is None:
|
||||
return epoch, trimmed, _STABLE_SUFFIX
|
||||
|
||||
# We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
|
||||
# We'll do this by abusing the pre segment, but we _only_ want to do this
|
||||
# if there is not a pre or a post segment. If we have one of those then
|
||||
# the normal sorting rules will handle this case correctly.
|
||||
if pre is None and post is None and dev is not None:
|
||||
_pre: CmpPrePostDevType = NegativeInfinity
|
||||
# Versions without a pre-release (except as noted above) should sort after
|
||||
# those with one.
|
||||
# dev-only (e.g. 1.0.dev1) sorts before all pre-releases.
|
||||
pre_rank, pre_n = _PRE_RANK_DEV_ONLY, 0
|
||||
elif pre is None:
|
||||
_pre = Infinity
|
||||
pre_rank, pre_n = _PRE_RANK_STABLE, 0
|
||||
else:
|
||||
_pre = pre
|
||||
pre_rank, pre_n = _PRE_RANK[pre[0]], pre[1]
|
||||
|
||||
# Versions without a post segment should sort before those with one.
|
||||
if post is None:
|
||||
_post: CmpPrePostDevType = NegativeInfinity
|
||||
post_rank = 0 if post is None else 1
|
||||
post_n = 0 if post is None else post[1]
|
||||
|
||||
else:
|
||||
_post = post
|
||||
dev_rank = 1 if dev is None else 0
|
||||
dev_n = 0 if dev is None else dev[1]
|
||||
|
||||
# Versions without a development segment should sort after those with one.
|
||||
if dev is None:
|
||||
_dev: CmpPrePostDevType = Infinity
|
||||
|
||||
else:
|
||||
_dev = dev
|
||||
suffix = (pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)
|
||||
|
||||
if local is None:
|
||||
# Versions without a local segment should sort before those with one.
|
||||
_local: CmpLocalType = NegativeInfinity
|
||||
else:
|
||||
# Versions with a local segment need that segment parsed to implement
|
||||
# the sorting rules in PEP440.
|
||||
# - Alpha numeric segments sort before numeric segments
|
||||
# - Alpha numeric segments sort lexicographically
|
||||
# - Numeric segments sort numerically
|
||||
# - Shorter versions sort before longer versions when the prefixes
|
||||
# match exactly
|
||||
_local = tuple(
|
||||
(i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
|
||||
)
|
||||
return epoch, trimmed, suffix
|
||||
|
||||
return epoch, _release, _pre, _post, _dev, _local
|
||||
cmp_local: CmpLocalType = tuple(
|
||||
(seg, "") if isinstance(seg, int) else (_LOCAL_STR_RANK, seg) for seg in local
|
||||
)
|
||||
return epoch, trimmed, suffix, cmp_local
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue