Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -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