Voice et bot modif

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

View file

@ -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