Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/version.py: 23%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
4"""
5.. testsetup::
7 from packaging.version import parse, normalize_pre, Version
8"""
10from __future__ import annotations
12import re
13import sys
14import typing
15from typing import (
16 Any,
17 Callable,
18 Literal,
19 NamedTuple,
20 SupportsInt,
21 Tuple,
22 TypedDict,
23 Union,
24)
26from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
28if typing.TYPE_CHECKING:
29 from typing_extensions import Self, Unpack
31if sys.version_info >= (3, 13): # pragma: no cover
32 from warnings import deprecated as _deprecated
33elif typing.TYPE_CHECKING:
34 from typing_extensions import deprecated as _deprecated
35else: # pragma: no cover
36 import functools
37 import warnings
39 def _deprecated(message: str) -> object:
40 def decorator(func: Callable[[...], object]) -> object:
41 @functools.wraps(func)
42 def wrapper(*args: object, **kwargs: object) -> object:
43 warnings.warn(
44 message,
45 category=DeprecationWarning,
46 stacklevel=2,
47 )
48 return func(*args, **kwargs)
50 return wrapper
52 return decorator
55_LETTER_NORMALIZATION = {
56 "alpha": "a",
57 "beta": "b",
58 "c": "rc",
59 "pre": "rc",
60 "preview": "rc",
61 "rev": "post",
62 "r": "post",
63}
65__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "normalize_pre", "parse"]
68def __dir__() -> list[str]:
69 return __all__
72LocalType = Tuple[Union[int, str], ...]
74CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]]
75CmpLocalType = Union[
76 NegativeInfinityType,
77 Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...],
78]
79CmpKey = Tuple[
80 int,
81 Tuple[int, ...],
82 CmpPrePostDevType,
83 CmpPrePostDevType,
84 CmpPrePostDevType,
85 CmpLocalType,
86]
87VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
90class _VersionReplace(TypedDict, total=False):
91 epoch: int | None
92 release: tuple[int, ...] | None
93 pre: tuple[str, int] | None
94 post: int | None
95 dev: int | None
96 local: str | None
99def normalize_pre(letter: str, /) -> str:
100 """Normalize the pre-release segment of a version string.
102 Returns a lowercase version of the string if not a known pre-release
103 identifier.
105 >>> normalize_pre('alpha')
106 'a'
107 >>> normalize_pre('BETA')
108 'b'
109 >>> normalize_pre('rc')
110 'rc'
112 :param letter:
114 .. versionadded:: 26.1
115 """
116 letter = letter.lower()
117 return _LETTER_NORMALIZATION.get(letter, letter)
120def parse(version: str) -> Version:
121 """Parse the given version string.
123 This is identical to the :class:`Version` constructor.
125 >>> parse('1.0.dev1')
126 <Version('1.0.dev1')>
128 :param version: The version string to parse.
129 :raises InvalidVersion: When the version string is not a valid version.
130 """
131 return Version(version)
134class InvalidVersion(ValueError):
135 """Raised when a version string is not a valid version.
137 >>> Version("invalid")
138 Traceback (most recent call last):
139 ...
140 packaging.version.InvalidVersion: Invalid version: 'invalid'
141 """
144class _BaseVersion:
145 __slots__ = ()
147 # This can also be a normal member (see the packaging_legacy package);
148 # we are just requiring it to be readable. Actually defining a property
149 # has runtime effect on subclasses, so it's typing only.
150 if typing.TYPE_CHECKING:
152 @property
153 def _key(self) -> tuple[Any, ...]: ...
155 def __hash__(self) -> int:
156 return hash(self._key)
158 # Please keep the duplicated `isinstance` check
159 # in the six comparisons hereunder
160 # unless you find a way to avoid adding overhead function calls.
161 def __lt__(self, other: _BaseVersion) -> bool:
162 if not isinstance(other, _BaseVersion):
163 return NotImplemented
165 return self._key < other._key
167 def __le__(self, other: _BaseVersion) -> bool:
168 if not isinstance(other, _BaseVersion):
169 return NotImplemented
171 return self._key <= other._key
173 def __eq__(self, other: object) -> bool:
174 if not isinstance(other, _BaseVersion):
175 return NotImplemented
177 return self._key == other._key
179 def __ge__(self, other: _BaseVersion) -> bool:
180 if not isinstance(other, _BaseVersion):
181 return NotImplemented
183 return self._key >= other._key
185 def __gt__(self, other: _BaseVersion) -> bool:
186 if not isinstance(other, _BaseVersion):
187 return NotImplemented
189 return self._key > other._key
191 def __ne__(self, other: object) -> bool:
192 if not isinstance(other, _BaseVersion):
193 return NotImplemented
195 return self._key != other._key
198# Deliberately not anchored to the start and end of the string, to make it
199# easier for 3rd party code to reuse
201# Note that ++ doesn't behave identically on CPython and PyPy, so not using it here
202_VERSION_PATTERN = r"""
203 v?+ # optional leading v
204 (?:
205 (?:(?P<epoch>[0-9]+)!)?+ # epoch
206 (?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment
207 (?P<pre> # pre-release
208 [._-]?+
209 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
210 [._-]?+
211 (?P<pre_n>[0-9]+)?
212 )?+
213 (?P<post> # post release
214 (?:-(?P<post_n1>[0-9]+))
215 |
216 (?:
217 [._-]?
218 (?P<post_l>post|rev|r)
219 [._-]?
220 (?P<post_n2>[0-9]+)?
221 )
222 )?+
223 (?P<dev> # dev release
224 [._-]?+
225 (?P<dev_l>dev)
226 [._-]?+
227 (?P<dev_n>[0-9]+)?
228 )?+
229 )
230 (?:\+
231 (?P<local> # local version
232 [a-z0-9]+
233 (?:[._-][a-z0-9]+)*+
234 )
235 )?+
236"""
238_VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?")
240# Possessive qualifiers were added in Python 3.11.
241# CPython 3.11.0-3.11.4 had a bug: https://github.com/python/cpython/pull/107795
242# Older PyPy also had a bug.
243VERSION_PATTERN = (
244 _VERSION_PATTERN_OLD
245 if (sys.implementation.name == "cpython" and sys.version_info < (3, 11, 5))
246 or (sys.implementation.name == "pypy" and sys.version_info < (3, 11, 13))
247 or sys.version_info < (3, 11)
248 else _VERSION_PATTERN
249)
250"""
251A string containing the regular expression used to match a valid version.
253The pattern is not anchored at either end, and is intended for embedding in larger
254expressions (for example, matching a version number as part of a file name). The
255regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
256flags set.
258.. versionchanged:: 26.0
260 The regex now uses possessive qualifiers on Python 3.11 if they are
261 supported (CPython 3.11.5+, PyPy 3.11.13+).
263:meta hide-value:
264"""
267# Validation pattern for local version in replace()
268_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE)
270# Fast path: If a version has only digits and dots then we
271# can skip the regex and parse it as a release segment
272_SIMPLE_VERSION_INDICATORS = frozenset(".0123456789")
275def _validate_epoch(value: object, /) -> int:
276 epoch = value or 0
277 if isinstance(epoch, int) and epoch >= 0:
278 return epoch
279 msg = f"epoch must be non-negative integer, got {epoch}"
280 raise InvalidVersion(msg)
283def _validate_release(value: object, /) -> tuple[int, ...]:
284 release = (0,) if value is None else value
285 if (
286 isinstance(release, tuple)
287 and len(release) > 0
288 and all(isinstance(i, int) and i >= 0 for i in release)
289 ):
290 return release
291 msg = f"release must be a non-empty tuple of non-negative integers, got {release}"
292 raise InvalidVersion(msg)
295def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None:
296 if value is None:
297 return value
298 if isinstance(value, tuple) and len(value) == 2:
299 letter, number = value
300 letter = normalize_pre(letter)
301 if letter in {"a", "b", "rc"} and isinstance(number, int) and number >= 0:
302 # type checkers can't infer the Literal type here on letter
303 return (letter, number) # type: ignore[return-value]
304 msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}"
305 raise InvalidVersion(msg)
308def _validate_post(value: object, /) -> tuple[Literal["post"], int] | None:
309 if value is None:
310 return value
311 if isinstance(value, int) and value >= 0:
312 return ("post", value)
313 msg = f"post must be non-negative integer, got {value}"
314 raise InvalidVersion(msg)
317def _validate_dev(value: object, /) -> tuple[Literal["dev"], int] | None:
318 if value is None:
319 return value
320 if isinstance(value, int) and value >= 0:
321 return ("dev", value)
322 msg = f"dev must be non-negative integer, got {value}"
323 raise InvalidVersion(msg)
326def _validate_local(value: object, /) -> LocalType | None:
327 if value is None:
328 return value
329 if isinstance(value, str) and _LOCAL_PATTERN.fullmatch(value):
330 return _parse_local_version(value)
331 msg = f"local must be a valid version string, got {value!r}"
332 raise InvalidVersion(msg)
335# Backward compatibility for internals before 26.0. Do not use.
336class _Version(NamedTuple):
337 epoch: int
338 release: tuple[int, ...]
339 dev: tuple[Literal["dev"], int] | None
340 pre: tuple[Literal["a", "b", "rc"], int] | None
341 post: tuple[Literal["post"], int] | None
342 local: LocalType | None
345class Version(_BaseVersion):
346 """This class abstracts handling of a project's versions.
348 A :class:`Version` instance is comparison aware and can be compared and
349 sorted using the standard Python interfaces.
351 >>> v1 = Version("1.0a5")
352 >>> v2 = Version("1.0")
353 >>> v1
354 <Version('1.0a5')>
355 >>> v2
356 <Version('1.0')>
357 >>> v1 < v2
358 True
359 >>> v1 == v2
360 False
361 >>> v1 > v2
362 False
363 >>> v1 >= v2
364 False
365 >>> v1 <= v2
366 True
368 :class:`Version` is immutable; use :meth:`__replace__` to change
369 part of a version.
370 """
372 __slots__ = ("_dev", "_epoch", "_key_cache", "_local", "_post", "_pre", "_release")
373 __match_args__ = ("_str",)
374 """
375 Pattern matching is supported on Python 3.10+.
377 .. versionadded:: 26.0
379 :meta hide-value:
380 """
382 _regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE)
384 _epoch: int
385 _release: tuple[int, ...]
386 _dev: tuple[Literal["dev"], int] | None
387 _pre: tuple[Literal["a", "b", "rc"], int] | None
388 _post: tuple[Literal["post"], int] | None
389 _local: LocalType | None
391 _key_cache: CmpKey | None
393 def __init__(self, version: str) -> None:
394 """Initialize a Version object.
396 :param version:
397 The string representation of a version which will be parsed and normalized
398 before use.
399 :raises InvalidVersion:
400 If the ``version`` does not conform to PEP 440 in any way then this
401 exception will be raised.
402 """
403 if _SIMPLE_VERSION_INDICATORS.issuperset(version):
404 try:
405 self._release = tuple(map(int, version.split(".")))
406 except ValueError:
407 raise InvalidVersion(f"Invalid version: {version!r}") from None
409 self._epoch = 0
410 self._pre = None
411 self._post = None
412 self._dev = None
413 self._local = None
414 self._key_cache = None
415 return
417 # Validate the version and parse it into pieces
418 match = self._regex.fullmatch(version)
419 if not match:
420 raise InvalidVersion(f"Invalid version: {version!r}")
421 self._epoch = int(match.group("epoch")) if match.group("epoch") else 0
422 self._release = tuple(map(int, match.group("release").split(".")))
423 # We can type ignore the assignments below because the regex guarantees
424 # the correct strings
425 self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n")) # type: ignore[assignment]
426 self._post = _parse_letter_version( # type: ignore[assignment]
427 match.group("post_l"), match.group("post_n1") or match.group("post_n2")
428 )
429 self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n")) # type: ignore[assignment]
430 self._local = _parse_local_version(match.group("local"))
432 # Key which will be used for sorting
433 self._key_cache = None
435 @classmethod
436 def from_parts(
437 cls,
438 *,
439 epoch: int = 0,
440 release: tuple[int, ...],
441 pre: tuple[str, int] | None = None,
442 post: int | None = None,
443 dev: int | None = None,
444 local: str | None = None,
445 ) -> Self:
446 """
447 Return a new version composed of the various parts.
449 This allows you to build a version without going though a string and
450 running a regular expression. It normalizes pre-release strings. The
451 ``release=`` keyword argument is required.
453 >>> Version.from_parts(release=(1,2,3))
454 <Version('1.2.3')>
455 >>> Version.from_parts(release=(0,1,0), pre=("b", 1))
456 <Version('0.1.0b1')>
458 :param epoch:
459 :param release: This version tuple is required
461 .. versionadded:: 26.0
462 """
463 _epoch = _validate_epoch(epoch)
464 _release = _validate_release(release)
465 _pre = _validate_pre(pre) if pre is not None else None
466 _post = _validate_post(post) if post is not None else None
467 _dev = _validate_dev(dev) if dev is not None else None
468 _local = _validate_local(local) if local is not None else None
470 new_version = cls.__new__(cls)
471 new_version._key_cache = None
472 new_version._epoch = _epoch
473 new_version._release = _release
474 new_version._pre = _pre
475 new_version._post = _post
476 new_version._dev = _dev
477 new_version._local = _local
479 return new_version
481 def __replace__(self, **kwargs: Unpack[_VersionReplace]) -> Self:
482 """
483 __replace__(*, epoch=..., release=..., pre=..., post=..., dev=..., local=...)
485 Return a new version with parts replaced.
487 This returns a new version (unless no parts were changed). The
488 pre-release is normalized. Setting a value to ``None`` clears it.
490 >>> v = Version("1.2.3")
491 >>> v.__replace__(pre=("a", 1))
492 <Version('1.2.3a1')>
494 :param int | None epoch:
495 :param tuple[int, ...] | None release:
496 :param tuple[str, int] | None pre:
497 :param int | None post:
498 :param int | None dev:
499 :param str | None local:
501 .. versionadded:: 26.0
502 .. versionchanged:: 26.1
504 The pre-release portion is now normalized.
505 """
506 epoch = _validate_epoch(kwargs["epoch"]) if "epoch" in kwargs else self._epoch
507 release = (
508 _validate_release(kwargs["release"])
509 if "release" in kwargs
510 else self._release
511 )
512 pre = _validate_pre(kwargs["pre"]) if "pre" in kwargs else self._pre
513 post = _validate_post(kwargs["post"]) if "post" in kwargs else self._post
514 dev = _validate_dev(kwargs["dev"]) if "dev" in kwargs else self._dev
515 local = _validate_local(kwargs["local"]) if "local" in kwargs else self._local
517 if (
518 epoch == self._epoch
519 and release == self._release
520 and pre == self._pre
521 and post == self._post
522 and dev == self._dev
523 and local == self._local
524 ):
525 return self
527 new_version = self.__class__.__new__(self.__class__)
528 new_version._key_cache = None
529 new_version._epoch = epoch
530 new_version._release = release
531 new_version._pre = pre
532 new_version._post = post
533 new_version._dev = dev
534 new_version._local = local
536 return new_version
538 @property
539 def _key(self) -> CmpKey:
540 if self._key_cache is None:
541 self._key_cache = _cmpkey(
542 self._epoch,
543 self._release,
544 self._pre,
545 self._post,
546 self._dev,
547 self._local,
548 )
549 return self._key_cache
551 @property
552 @_deprecated("Version._version is private and will be removed soon")
553 def _version(self) -> _Version:
554 return _Version(
555 self._epoch, self._release, self._dev, self._pre, self._post, self._local
556 )
558 @_version.setter
559 @_deprecated("Version._version is private and will be removed soon")
560 def _version(self, value: _Version) -> None:
561 self._epoch = value.epoch
562 self._release = value.release
563 self._dev = value.dev
564 self._pre = value.pre
565 self._post = value.post
566 self._local = value.local
567 self._key_cache = None
569 def __repr__(self) -> str:
570 """A representation of the Version that shows all internal state.
572 >>> Version('1.0.0')
573 <Version('1.0.0')>
574 """
575 return f"<{self.__class__.__name__}({str(self)!r})>"
577 def __str__(self) -> str:
578 """A string representation of the version that can be round-tripped.
580 >>> str(Version("1.0a5"))
581 '1.0a5'
582 """
583 # This is a hot function, so not calling self.base_version
584 version = ".".join(map(str, self.release))
586 # Epoch
587 if self.epoch:
588 version = f"{self.epoch}!{version}"
590 # Pre-release
591 if self.pre is not None:
592 version += "".join(map(str, self.pre))
594 # Post-release
595 if self.post is not None:
596 version += f".post{self.post}"
598 # Development release
599 if self.dev is not None:
600 version += f".dev{self.dev}"
602 # Local version segment
603 if self.local is not None:
604 version += f"+{self.local}"
606 return version
608 @property
609 def _str(self) -> str:
610 """Internal property for match_args"""
611 return str(self)
613 @property
614 def epoch(self) -> int:
615 """The epoch of the version.
617 >>> Version("2.0.0").epoch
618 0
619 >>> Version("1!2.0.0").epoch
620 1
621 """
622 return self._epoch
624 @property
625 def release(self) -> tuple[int, ...]:
626 """The components of the "release" segment of the version.
628 >>> Version("1.2.3").release
629 (1, 2, 3)
630 >>> Version("2.0.0").release
631 (2, 0, 0)
632 >>> Version("1!2.0.0.post0").release
633 (2, 0, 0)
635 Includes trailing zeroes but not the epoch or any pre-release / development /
636 post-release suffixes.
637 """
638 return self._release
640 @property
641 def pre(self) -> tuple[Literal["a", "b", "rc"], int] | None:
642 """The pre-release segment of the version.
644 >>> print(Version("1.2.3").pre)
645 None
646 >>> Version("1.2.3a1").pre
647 ('a', 1)
648 >>> Version("1.2.3b1").pre
649 ('b', 1)
650 >>> Version("1.2.3rc1").pre
651 ('rc', 1)
652 """
653 return self._pre
655 @property
656 def post(self) -> int | None:
657 """The post-release number of the version.
659 >>> print(Version("1.2.3").post)
660 None
661 >>> Version("1.2.3.post1").post
662 1
663 """
664 return self._post[1] if self._post else None
666 @property
667 def dev(self) -> int | None:
668 """The development number of the version.
670 >>> print(Version("1.2.3").dev)
671 None
672 >>> Version("1.2.3.dev1").dev
673 1
674 """
675 return self._dev[1] if self._dev else None
677 @property
678 def local(self) -> str | None:
679 """The local version segment of the version.
681 >>> print(Version("1.2.3").local)
682 None
683 >>> Version("1.2.3+abc").local
684 'abc'
685 """
686 if self._local:
687 return ".".join(str(x) for x in self._local)
688 else:
689 return None
691 @property
692 def public(self) -> str:
693 """The public portion of the version.
695 This returns a string. If you want a :class:`Version` again and care
696 about performance, use ``v.__replace__(local=None)`` instead.
698 >>> Version("1.2.3").public
699 '1.2.3'
700 >>> Version("1.2.3+abc").public
701 '1.2.3'
702 >>> Version("1!1.2.3dev1+abc").public
703 '1!1.2.3.dev1'
704 """
705 return str(self).split("+", 1)[0]
707 @property
708 def base_version(self) -> str:
709 """The "base version" of the version.
711 This returns a string. If you want a :class:`Version` again and care
712 about performance, use
713 ``v.__replace__(pre=None, post=None, dev=None, local=None)`` instead.
715 >>> Version("1.2.3").base_version
716 '1.2.3'
717 >>> Version("1.2.3+abc").base_version
718 '1.2.3'
719 >>> Version("1!1.2.3dev1+abc").base_version
720 '1!1.2.3'
722 The "base version" is the public version of the project without any pre or post
723 release markers.
724 """
725 release_segment = ".".join(map(str, self.release))
726 return f"{self.epoch}!{release_segment}" if self.epoch else release_segment
728 @property
729 def is_prerelease(self) -> bool:
730 """Whether this version is a pre-release.
732 >>> Version("1.2.3").is_prerelease
733 False
734 >>> Version("1.2.3a1").is_prerelease
735 True
736 >>> Version("1.2.3b1").is_prerelease
737 True
738 >>> Version("1.2.3rc1").is_prerelease
739 True
740 >>> Version("1.2.3dev1").is_prerelease
741 True
742 """
743 return self.dev is not None or self.pre is not None
745 @property
746 def is_postrelease(self) -> bool:
747 """Whether this version is a post-release.
749 >>> Version("1.2.3").is_postrelease
750 False
751 >>> Version("1.2.3.post1").is_postrelease
752 True
753 """
754 return self.post is not None
756 @property
757 def is_devrelease(self) -> bool:
758 """Whether this version is a development release.
760 >>> Version("1.2.3").is_devrelease
761 False
762 >>> Version("1.2.3.dev1").is_devrelease
763 True
764 """
765 return self.dev is not None
767 @property
768 def major(self) -> int:
769 """The first item of :attr:`release` or ``0`` if unavailable.
771 >>> Version("1.2.3").major
772 1
773 """
774 return self.release[0] if len(self.release) >= 1 else 0
776 @property
777 def minor(self) -> int:
778 """The second item of :attr:`release` or ``0`` if unavailable.
780 >>> Version("1.2.3").minor
781 2
782 >>> Version("1").minor
783 0
784 """
785 return self.release[1] if len(self.release) >= 2 else 0
787 @property
788 def micro(self) -> int:
789 """The third item of :attr:`release` or ``0`` if unavailable.
791 >>> Version("1.2.3").micro
792 3
793 >>> Version("1").micro
794 0
795 """
796 return self.release[2] if len(self.release) >= 3 else 0
799class _TrimmedRelease(Version):
800 __slots__ = ()
802 def __init__(self, version: str | Version) -> None:
803 if isinstance(version, Version):
804 self._epoch = version._epoch
805 self._release = version._release
806 self._dev = version._dev
807 self._pre = version._pre
808 self._post = version._post
809 self._local = version._local
810 self._key_cache = version._key_cache
811 return
812 super().__init__(version) # pragma: no cover
814 @property
815 def release(self) -> tuple[int, ...]:
816 """
817 Release segment without any trailing zeros.
819 >>> _TrimmedRelease('1.0.0').release
820 (1,)
821 >>> _TrimmedRelease('0.0').release
822 (0,)
823 """
824 # This leaves one 0.
825 rel = super().release
826 len_release = len(rel)
827 i = len_release
828 while i > 1 and rel[i - 1] == 0:
829 i -= 1
830 return rel if i == len_release else rel[:i]
833def _parse_letter_version(
834 letter: str | None, number: str | bytes | SupportsInt | None
835) -> tuple[str, int] | None:
836 if letter:
837 # We normalize any letters to their lower case form
838 letter = letter.lower()
840 # We consider some words to be alternate spellings of other words and
841 # in those cases we want to normalize the spellings to our preferred
842 # spelling.
843 letter = _LETTER_NORMALIZATION.get(letter, letter)
845 # We consider there to be an implicit 0 in a pre-release if there is
846 # not a numeral associated with it.
847 return letter, int(number or 0)
849 if number:
850 # We assume if we are given a number, but we are not given a letter
851 # then this is using the implicit post release syntax (e.g. 1.0-1)
852 return "post", int(number)
854 return None
857_local_version_separators = re.compile(r"[\._-]")
860def _parse_local_version(local: str | None) -> LocalType | None:
861 """
862 Takes a string like ``"abc.1.twelve"`` and turns it into
863 ``("abc", 1, "twelve")``.
864 """
865 if local is not None:
866 return tuple(
867 part.lower() if not part.isdigit() else int(part)
868 for part in _local_version_separators.split(local)
869 )
870 return None
873def _cmpkey(
874 epoch: int,
875 release: tuple[int, ...],
876 pre: tuple[str, int] | None,
877 post: tuple[str, int] | None,
878 dev: tuple[str, int] | None,
879 local: LocalType | None,
880) -> CmpKey:
881 # When we compare a release version, we want to compare it with all of the
882 # trailing zeros removed. We will use this for our sorting key.
883 len_release = len(release)
884 i = len_release
885 while i and release[i - 1] == 0:
886 i -= 1
887 _release = release if i == len_release else release[:i]
889 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
890 # We'll do this by abusing the pre segment, but we _only_ want to do this
891 # if there is not a pre or a post segment. If we have one of those then
892 # the normal sorting rules will handle this case correctly.
893 if pre is None and post is None and dev is not None:
894 _pre: CmpPrePostDevType = NegativeInfinity
895 # Versions without a pre-release (except as noted above) should sort after
896 # those with one.
897 elif pre is None:
898 _pre = Infinity
899 else:
900 _pre = pre
902 # Versions without a post segment should sort before those with one.
903 if post is None:
904 _post: CmpPrePostDevType = NegativeInfinity
906 else:
907 _post = post
909 # Versions without a development segment should sort after those with one.
910 if dev is None:
911 _dev: CmpPrePostDevType = Infinity
913 else:
914 _dev = dev
916 if local is None:
917 # Versions without a local segment should sort before those with one.
918 _local: CmpLocalType = NegativeInfinity
919 else:
920 # Versions with a local segment need that segment parsed to implement
921 # the sorting rules in PEP440.
922 # - Alpha numeric segments sort before numeric segments
923 # - Alpha numeric segments sort lexicographically
924 # - Numeric segments sort numerically
925 # - Shorter versions sort before longer versions when the prefixes
926 # match exactly
927 _local = tuple(
928 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
929 )
931 return epoch, _release, _pre, _post, _dev, _local