Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/version.py: 24%
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, _cmpkey
8"""
10from __future__ import annotations
12__lazy_modules__ = {"functools"}
14import builtins
15import re
16import sys
17import typing
18from collections.abc import Callable
19from typing import (
20 Any,
21 Literal,
22 NamedTuple,
23 SupportsInt,
24)
26if typing.TYPE_CHECKING:
27 from typing_extensions import Self
29if sys.version_info >= (3, 13): # pragma: no cover
30 from warnings import deprecated as _deprecated
31elif typing.TYPE_CHECKING:
32 from typing_extensions import deprecated as _deprecated
33else: # pragma: no cover
34 import functools
35 import warnings
37 def _deprecated(message: str) -> object:
38 def decorator(func: Callable[[...], object]) -> object:
39 @functools.wraps(func)
40 def wrapper(*args: object, **kwargs: object) -> object:
41 warnings.warn(
42 message,
43 category=DeprecationWarning,
44 stacklevel=2,
45 )
46 return func(*args, **kwargs)
48 return wrapper
50 return decorator
53_LETTER_NORMALIZATION = {
54 "alpha": "a",
55 "beta": "b",
56 "c": "rc",
57 "pre": "rc",
58 "preview": "rc",
59 "rev": "post",
60 "r": "post",
61}
63__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "normalize_pre", "parse"]
66def __dir__() -> list[str]:
67 return __all__
70LocalType = tuple[int | str, ...]
72CmpLocalType = tuple[tuple[int, str], ...]
73CmpSuffix = tuple[int, int, int, int, int, int]
74CmpKey = (
75 tuple[int, tuple[int, ...], CmpSuffix]
76 | tuple[int, tuple[int, ...], CmpSuffix, CmpLocalType]
77)
78VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
81# Sentinel for __replace__, typed as Any so it can be the default for any field.
82# mypy is missing sentinel currently
83_UNSET: Any = object() if sys.version_info < (3, 15) else builtins.sentinel("_UNSET") # type: ignore[attr-defined]
86def normalize_pre(letter: str, /) -> str:
87 """Normalize the pre-release segment of a version string.
89 Returns a lowercase version of the string if not a known pre-release
90 identifier.
92 >>> normalize_pre('alpha')
93 'a'
94 >>> normalize_pre('BETA')
95 'b'
96 >>> normalize_pre('rc')
97 'rc'
99 :param letter:
101 .. versionadded:: 26.1
102 """
103 letter = letter.lower()
104 return _LETTER_NORMALIZATION.get(letter, letter)
107def parse(version: str) -> Version:
108 """Parse the given version string.
110 This is identical to the :class:`Version` constructor.
112 >>> parse('1.0.dev1')
113 <Version('1.0.dev1')>
115 :param version: The version string to parse.
116 :raises InvalidVersion: When the version string is not a valid version.
117 """
118 return Version(version)
121class InvalidVersion(ValueError):
122 """Raised when a version string is not a valid version.
124 >>> Version("invalid")
125 Traceback (most recent call last):
126 ...
127 packaging.version.InvalidVersion: Invalid version: 'invalid'
128 """
131class _BaseVersion:
132 __slots__ = ()
134 # This can also be a normal member (see the packaging_legacy package);
135 # we are just requiring it to be readable. Actually defining a property
136 # has runtime effect on subclasses, so it's typing only.
137 if typing.TYPE_CHECKING:
139 @property
140 def _key(self) -> tuple[Any, ...]: ...
142 def __hash__(self) -> int:
143 return hash(self._key)
145 # Please keep the duplicated `isinstance` check
146 # in the six comparisons hereunder
147 # unless you find a way to avoid adding overhead function calls.
148 def __lt__(self, other: _BaseVersion) -> bool:
149 if not isinstance(other, _BaseVersion):
150 return NotImplemented
152 return self._key < other._key
154 def __le__(self, other: _BaseVersion) -> bool:
155 if not isinstance(other, _BaseVersion):
156 return NotImplemented
158 return self._key <= other._key
160 def __eq__(self, other: object) -> bool:
161 if not isinstance(other, _BaseVersion):
162 return NotImplemented
164 return self._key == other._key
166 def __ge__(self, other: _BaseVersion) -> bool:
167 if not isinstance(other, _BaseVersion):
168 return NotImplemented
170 return self._key >= other._key
172 def __gt__(self, other: _BaseVersion) -> bool:
173 if not isinstance(other, _BaseVersion):
174 return NotImplemented
176 return self._key > other._key
178 def __ne__(self, other: object) -> bool:
179 if not isinstance(other, _BaseVersion):
180 return NotImplemented
182 return self._key != other._key
185# Deliberately not anchored to the start and end of the string, to make it
186# easier for 3rd party code to reuse
188# Note that ++ doesn't behave identically on CPython and PyPy, so not using it here
189_VERSION_PATTERN = r"""
190 v?+ # optional leading v
191 (?a:
192 (?:(?P<epoch>[0-9]+)!)?+ # epoch
193 (?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment
194 (?P<pre> # pre-release
195 [._-]?+
196 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
197 [._-]?+
198 (?P<pre_n>[0-9]+)?
199 )?+
200 (?P<post> # post release
201 (?:-(?P<post_n1>[0-9]+))
202 |
203 (?:
204 [._-]?
205 (?P<post_l>post|rev|r)
206 [._-]?
207 (?P<post_n2>[0-9]+)?
208 )
209 )?+
210 (?P<dev> # dev release
211 [._-]?+
212 (?P<dev_l>dev)
213 [._-]?+
214 (?P<dev_n>[0-9]+)?
215 )?+
216 )
217 (?a:\+
218 (?P<local> # local version
219 [a-z0-9]+
220 (?:[._-][a-z0-9]+)*+
221 )
222 )?+
223"""
225_VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?")
227# Possessive qualifiers were added in Python 3.11.
228# CPython 3.11.0-3.11.4 had a bug: https://github.com/python/cpython/pull/107795
229# Older PyPy also had a bug.
230VERSION_PATTERN = (
231 _VERSION_PATTERN_OLD
232 if (sys.implementation.name == "cpython" and sys.version_info < (3, 11, 5))
233 or (sys.implementation.name == "pypy" and sys.version_info < (3, 11, 13))
234 or sys.version_info < (3, 11)
235 else _VERSION_PATTERN
236)
237"""
238A string containing the regular expression used to match a valid version.
240The pattern is not anchored at either end, and is intended for embedding in larger
241expressions (for example, matching a version number as part of a file name). The
242regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
243flags set.
245.. versionchanged:: 26.0
247 The regex now uses possessive qualifiers on Python 3.11 if they are
248 supported (CPython 3.11.5+, PyPy 3.11.13+).
250:meta hide-value:
251"""
254# Validation pattern for local version in replace()
255_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE | re.ASCII)
257# Fast path: If a version has only digits and dots then we
258# can skip the regex and parse it as a release segment
259_SIMPLE_VERSION_INDICATORS = frozenset(".0123456789")
262def _validate_epoch(value: object, /) -> int:
263 epoch = value or 0
264 if isinstance(epoch, int) and epoch >= 0:
265 return epoch
266 msg = f"epoch must be non-negative integer, got {epoch}"
267 raise InvalidVersion(msg)
270def _validate_release(value: object, /) -> tuple[int, ...]:
271 release = (0,) if value is None else value
272 if (
273 isinstance(release, tuple)
274 and len(release) > 0
275 and all(isinstance(i, int) and i >= 0 for i in release)
276 ):
277 return release
278 msg = f"release must be a non-empty tuple of non-negative integers, got {release}"
279 raise InvalidVersion(msg)
282def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None:
283 if value is None:
284 return value
285 if isinstance(value, tuple) and len(value) == 2:
286 letter, number = value
287 # The letter must be a string before it can be normalized.
288 if (
289 isinstance(letter, str)
290 and (normalized := normalize_pre(letter)) in {"a", "b", "rc"}
291 and isinstance(number, int)
292 and number >= 0
293 ):
294 # type checkers can't infer the Literal type here on letter
295 return (normalized, number) # type: ignore[return-value]
296 msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}"
297 raise InvalidVersion(msg)
300def _validate_post(value: object, /) -> tuple[Literal["post"], int] | None:
301 if value is None:
302 return value
303 if isinstance(value, int) and value >= 0:
304 return ("post", value)
305 msg = f"post must be non-negative integer, got {value}"
306 raise InvalidVersion(msg)
309def _validate_dev(value: object, /) -> tuple[Literal["dev"], int] | None:
310 if value is None:
311 return value
312 if isinstance(value, int) and value >= 0:
313 return ("dev", value)
314 msg = f"dev must be non-negative integer, got {value}"
315 raise InvalidVersion(msg)
318def _validate_local(value: object, /) -> LocalType | None:
319 if value is None:
320 return value
321 if isinstance(value, str) and _LOCAL_PATTERN.fullmatch(value):
322 return _parse_local_version(value)
323 msg = f"local must be a valid version string, got {value!r}"
324 raise InvalidVersion(msg)
327# Backward compatibility for internals before 26.0. Do not use.
328class _Version(NamedTuple):
329 epoch: int
330 release: tuple[int, ...]
331 dev: tuple[Literal["dev"], int] | None
332 pre: tuple[Literal["a", "b", "rc"], int] | None
333 post: tuple[Literal["post"], int] | None
334 local: LocalType | None
337class Version(_BaseVersion):
338 """This class abstracts handling of a project's versions.
340 A :class:`Version` instance is comparison aware and can be compared and
341 sorted using the standard Python interfaces.
343 >>> v1 = Version("1.0a5")
344 >>> v2 = Version("1.0")
345 >>> v1
346 <Version('1.0a5')>
347 >>> v2
348 <Version('1.0')>
349 >>> v1 < v2
350 True
351 >>> v1 == v2
352 False
353 >>> v1 > v2
354 False
355 >>> v1 >= v2
356 False
357 >>> v1 <= v2
358 True
360 :class:`Version` is immutable; use :meth:`__replace__` to change
361 part of a version.
363 Instances are safe to serialize with :mod:`pickle`. They use a stable
364 format so the same pickle can be loaded in future packaging releases.
366 .. versionchanged:: 26.2
368 Added a stable pickle format. Pickles created with packaging 26.2+ can
369 be unpickled with future releases. Backward compatibility with pickles
370 from packaging < 26.2 is supported but may be removed in a future
371 release.
372 """
374 __slots__ = (
375 "_dev",
376 "_epoch",
377 "_hash_cache",
378 "_key_cache",
379 "_local",
380 "_post",
381 "_pre",
382 "_release",
383 )
384 __match_args__ = ("_str",)
385 """
386 Pattern matching is supported on Python 3.10+.
388 .. versionadded:: 26.0
390 :meta hide-value:
391 """
393 _regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE)
395 _epoch: int
396 _release: tuple[int, ...]
397 _dev: tuple[Literal["dev"], int] | None
398 _pre: tuple[Literal["a", "b", "rc"], int] | None
399 _post: tuple[Literal["post"], int] | None
400 _local: LocalType | None
402 _hash_cache: int | None
403 _key_cache: CmpKey | None
405 def __init__(self, version: str) -> None:
406 """Initialize a Version object.
408 :param version:
409 The string representation of a version which will be parsed and normalized
410 before use.
411 :raises InvalidVersion:
412 If the ``version`` does not conform to PEP 440 in any way then this
413 exception will be raised.
414 """
415 try:
416 is_simple = _SIMPLE_VERSION_INDICATORS.issuperset(version)
417 except TypeError:
418 raise InvalidVersion(f"Invalid version: {version!r}") from None
420 if is_simple:
421 try:
422 self._release = tuple(map(int, version.split(".")))
423 except AttributeError:
424 raise InvalidVersion(f"Invalid version: {version!r}") from None
425 except ValueError:
426 # Empty parts (from "1..2", ".1", etc.) are invalid versions.
427 # Any other ValueError (e.g. int str-digits limit) should
428 # propagate to the caller.
429 if "" in version.split("."):
430 raise InvalidVersion(f"Invalid version: {version!r}") from None
431 raise
433 self._epoch = 0
434 self._pre = None
435 self._post = None
436 self._dev = None
437 self._local = None
438 self._key_cache = None
439 self._hash_cache = None
440 return
442 # Validate the version and parse it into pieces
443 try:
444 match = self._regex.fullmatch(version)
445 except TypeError:
446 raise InvalidVersion(f"Invalid version: {version!r}") from None
447 if not match:
448 raise InvalidVersion(f"Invalid version: {version!r}")
449 self._epoch = int(match.group("epoch")) if match.group("epoch") else 0
450 self._release = tuple(map(int, match.group("release").split(".")))
451 # We can type ignore the assignments below because the regex guarantees
452 # the correct strings
453 self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n")) # type: ignore[assignment]
454 self._post = _parse_letter_version( # type: ignore[assignment]
455 match.group("post_l"), match.group("post_n1") or match.group("post_n2")
456 )
457 self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n")) # type: ignore[assignment]
458 self._local = _parse_local_version(match.group("local"))
460 # Key which will be used for sorting
461 self._key_cache = None
462 self._hash_cache = None
464 @classmethod
465 def from_parts(
466 cls,
467 *,
468 epoch: int = 0,
469 release: tuple[int, ...],
470 pre: tuple[str, int] | None = None,
471 post: int | None = None,
472 dev: int | None = None,
473 local: str | None = None,
474 ) -> Self:
475 """
476 Return a new version composed of the various parts.
478 This allows you to build a version without going though a string and
479 running a regular expression. It normalizes pre-release strings. The
480 ``release=`` keyword argument is required.
482 >>> Version.from_parts(release=(1,2,3))
483 <Version('1.2.3')>
484 >>> Version.from_parts(release=(0,1,0), pre=("b", 1))
485 <Version('0.1.0b1')>
487 :param epoch:
488 :param release: This version tuple is required
490 .. versionadded:: 26.1
491 """
492 _epoch = _validate_epoch(epoch)
493 _release = _validate_release(release)
494 _pre = _validate_pre(pre) if pre is not None else None
495 _post = _validate_post(post) if post is not None else None
496 _dev = _validate_dev(dev) if dev is not None else None
497 _local = _validate_local(local) if local is not None else None
499 new_version = cls.__new__(cls)
500 new_version._key_cache = None
501 new_version._hash_cache = None
502 new_version._epoch = _epoch
503 new_version._release = _release
504 new_version._pre = _pre
505 new_version._post = _post
506 new_version._dev = _dev
507 new_version._local = _local
509 return new_version
511 def __replace__(
512 self,
513 *,
514 epoch: int | None = _UNSET,
515 release: tuple[int, ...] | None = _UNSET,
516 pre: tuple[str, int] | None = _UNSET,
517 post: int | None = _UNSET,
518 dev: int | None = _UNSET,
519 local: str | None = _UNSET,
520 ) -> Self:
521 """
522 __replace__(*, epoch=..., release=..., pre=..., post=..., dev=..., local=...)
524 Return a new version with parts replaced.
526 This returns a new version (unless no parts were changed). The
527 pre-release is normalized. Setting a value to ``None`` clears it.
529 >>> v = Version("1.2.3")
530 >>> v.__replace__(pre=("a", 1))
531 <Version('1.2.3a1')>
533 :param int | None epoch:
534 :param tuple[int, ...] | None release:
535 :param tuple[str, int] | None pre:
536 :param int | None post:
537 :param int | None dev:
538 :param str | None local:
540 .. versionadded:: 26.0
541 .. versionchanged:: 26.1
543 The pre-release portion is now normalized.
545 .. versionchanged:: 26.4
547 Unknown keyword arguments now raise :exc:`TypeError` instead of
548 being silently ignored.
549 """
550 _epoch = self._epoch if epoch is _UNSET else _validate_epoch(epoch)
551 _release = self._release if release is _UNSET else _validate_release(release)
552 _pre = self._pre if pre is _UNSET else _validate_pre(pre)
553 _post = self._post if post is _UNSET else _validate_post(post)
554 _dev = self._dev if dev is _UNSET else _validate_dev(dev)
555 _local = self._local if local is _UNSET else _validate_local(local)
557 if (
558 _epoch == self._epoch
559 and _release == self._release
560 and _pre == self._pre
561 and _post == self._post
562 and _dev == self._dev
563 and _local == self._local
564 ):
565 return self
567 new_version = self.__class__.__new__(self.__class__)
568 new_version._key_cache = None
569 new_version._hash_cache = None
570 new_version._epoch = _epoch
571 new_version._release = _release
572 new_version._pre = _pre
573 new_version._post = _post
574 new_version._dev = _dev
575 new_version._local = _local
577 return new_version
579 @property
580 def _key(self) -> CmpKey:
581 if self._key_cache is None:
582 self._key_cache = _cmpkey(
583 self._epoch,
584 self._release,
585 self._pre,
586 self._post,
587 self._dev,
588 self._local,
589 )
590 return self._key_cache
592 # __hash__ must be defined when __eq__ is overridden,
593 # otherwise Python sets __hash__ to None.
594 def __hash__(self) -> int:
595 if (cached_hash := self._hash_cache) is not None:
596 return cached_hash
598 if (key := self._key_cache) is None:
599 self._key_cache = key = _cmpkey(
600 self._epoch,
601 self._release,
602 self._pre,
603 self._post,
604 self._dev,
605 self._local,
606 )
607 self._hash_cache = cached_hash = hash(key)
608 return cached_hash
610 # Override comparison methods to use direct _key_cache access
611 # This is faster than property access, especially before Python 3.12
612 def __lt__(self, other: _BaseVersion) -> bool:
613 if isinstance(other, Version):
614 if self._key_cache is None:
615 self._key_cache = _cmpkey(
616 self._epoch,
617 self._release,
618 self._pre,
619 self._post,
620 self._dev,
621 self._local,
622 )
623 if other._key_cache is None:
624 other._key_cache = _cmpkey(
625 other._epoch,
626 other._release,
627 other._pre,
628 other._post,
629 other._dev,
630 other._local,
631 )
632 return self._key_cache < other._key_cache
634 if not isinstance(other, _BaseVersion):
635 return NotImplemented
637 return super().__lt__(other)
639 def __le__(self, other: _BaseVersion) -> bool:
640 if isinstance(other, Version):
641 if self._key_cache is None:
642 self._key_cache = _cmpkey(
643 self._epoch,
644 self._release,
645 self._pre,
646 self._post,
647 self._dev,
648 self._local,
649 )
650 if other._key_cache is None:
651 other._key_cache = _cmpkey(
652 other._epoch,
653 other._release,
654 other._pre,
655 other._post,
656 other._dev,
657 other._local,
658 )
659 return self._key_cache <= other._key_cache
661 if not isinstance(other, _BaseVersion):
662 return NotImplemented
664 return super().__le__(other)
666 def __eq__(self, other: object) -> bool:
667 if isinstance(other, Version):
668 if self._key_cache is None:
669 self._key_cache = _cmpkey(
670 self._epoch,
671 self._release,
672 self._pre,
673 self._post,
674 self._dev,
675 self._local,
676 )
677 if other._key_cache is None:
678 other._key_cache = _cmpkey(
679 other._epoch,
680 other._release,
681 other._pre,
682 other._post,
683 other._dev,
684 other._local,
685 )
686 return self._key_cache == other._key_cache
688 if not isinstance(other, _BaseVersion):
689 return NotImplemented
691 return super().__eq__(other)
693 def __ge__(self, other: _BaseVersion) -> bool:
694 if isinstance(other, Version):
695 if self._key_cache is None:
696 self._key_cache = _cmpkey(
697 self._epoch,
698 self._release,
699 self._pre,
700 self._post,
701 self._dev,
702 self._local,
703 )
704 if other._key_cache is None:
705 other._key_cache = _cmpkey(
706 other._epoch,
707 other._release,
708 other._pre,
709 other._post,
710 other._dev,
711 other._local,
712 )
713 return self._key_cache >= other._key_cache
715 if not isinstance(other, _BaseVersion):
716 return NotImplemented
718 return super().__ge__(other)
720 def __gt__(self, other: _BaseVersion) -> bool:
721 if isinstance(other, Version):
722 if self._key_cache is None:
723 self._key_cache = _cmpkey(
724 self._epoch,
725 self._release,
726 self._pre,
727 self._post,
728 self._dev,
729 self._local,
730 )
731 if other._key_cache is None:
732 other._key_cache = _cmpkey(
733 other._epoch,
734 other._release,
735 other._pre,
736 other._post,
737 other._dev,
738 other._local,
739 )
740 return self._key_cache > other._key_cache
742 if not isinstance(other, _BaseVersion):
743 return NotImplemented
745 return super().__gt__(other)
747 def __ne__(self, other: object) -> bool:
748 if isinstance(other, Version):
749 if self._key_cache is None:
750 self._key_cache = _cmpkey(
751 self._epoch,
752 self._release,
753 self._pre,
754 self._post,
755 self._dev,
756 self._local,
757 )
758 if other._key_cache is None:
759 other._key_cache = _cmpkey(
760 other._epoch,
761 other._release,
762 other._pre,
763 other._post,
764 other._dev,
765 other._local,
766 )
767 return self._key_cache != other._key_cache
769 if not isinstance(other, _BaseVersion):
770 return NotImplemented
772 return super().__ne__(other)
774 def __getstate__(
775 self,
776 ) -> tuple[
777 int,
778 tuple[int, ...],
779 tuple[str, int] | None,
780 tuple[str, int] | None,
781 tuple[str, int] | None,
782 LocalType | None,
783 ]:
784 # Return state as a 6-item tuple for compactness:
785 # (epoch, release, pre, post, dev, local)
786 # Cache members are excluded and will be recomputed on demand
787 return (
788 self._epoch,
789 self._release,
790 self._pre,
791 self._post,
792 self._dev,
793 self._local,
794 )
796 def __setstate__(self, state: object) -> None:
797 # Always discard cached values — they may contain stale references
798 # (e.g. packaging._structures.InfinityType from pre-26.1 pickles)
799 # and will be recomputed on demand from the core fields above.
800 self._key_cache = None
801 self._hash_cache = None
803 if isinstance(state, tuple):
804 if len(state) == 6:
805 # New format (26.2+): (epoch, release, pre, post, dev, local)
806 (
807 self._epoch,
808 self._release,
809 self._pre,
810 self._post,
811 self._dev,
812 self._local,
813 ) = state
814 return
815 if len(state) == 2:
816 # Format (packaging 26.0-26.1): (None, {slot: value}).
817 _, slot_dict = state
818 if isinstance(slot_dict, dict):
819 self._epoch = slot_dict["_epoch"]
820 self._release = slot_dict["_release"]
821 self._pre = slot_dict.get("_pre")
822 self._post = slot_dict.get("_post")
823 self._dev = slot_dict.get("_dev")
824 self._local = slot_dict.get("_local")
825 return
826 if isinstance(state, dict):
827 # Old format (packaging <= 25.x, no __slots__): state is a plain
828 # dict with "_version" (_Version NamedTuple) and "_key" entries.
829 version_nt = state.get("_version")
830 if version_nt is not None:
831 self._epoch = version_nt.epoch
832 self._release = version_nt.release
833 self._pre = version_nt.pre
834 self._post = version_nt.post
835 self._dev = version_nt.dev
836 self._local = version_nt.local
837 return
839 raise TypeError(f"Cannot restore Version from {state!r}")
841 @property
842 @_deprecated("Version._version is private and will be removed soon")
843 def _version(self) -> _Version:
844 return _Version(
845 self._epoch, self._release, self._dev, self._pre, self._post, self._local
846 )
848 @_version.setter
849 @_deprecated("Version._version is private and will be removed soon")
850 def _version(self, value: _Version) -> None:
851 self._epoch = value.epoch
852 self._release = value.release
853 self._dev = value.dev
854 self._pre = value.pre
855 self._post = value.post
856 self._local = value.local
857 self._key_cache = None
858 self._hash_cache = None
860 def __repr__(self) -> str:
861 """A representation of the Version that shows all internal state.
863 >>> Version('1.0.0')
864 <Version('1.0.0')>
865 """
866 return f"<{self.__class__.__name__}({str(self)!r})>"
868 def __str__(self) -> str:
869 """A string representation of the version that can be round-tripped.
871 >>> str(Version("1.0a5"))
872 '1.0a5'
873 """
874 # This is a hot function, so not calling self.base_version
875 version = ".".join(map(str, self.release))
877 # Epoch
878 if self.epoch:
879 version = f"{self.epoch}!{version}"
881 # Pre-release
882 if self.pre is not None:
883 version += "".join(map(str, self.pre))
885 # Post-release
886 if self.post is not None:
887 version += f".post{self.post}"
889 # Development release
890 if self.dev is not None:
891 version += f".dev{self.dev}"
893 # Local version segment
894 if self.local is not None:
895 version += f"+{self.local}"
897 return version
899 @property
900 def _str(self) -> str:
901 """Internal property for match_args"""
902 return str(self)
904 @property
905 def epoch(self) -> int:
906 """The epoch of the version.
908 >>> Version("2.0.0").epoch
909 0
910 >>> Version("1!2.0.0").epoch
911 1
912 """
913 return self._epoch
915 @property
916 def release(self) -> tuple[int, ...]:
917 """The components of the "release" segment of the version.
919 >>> Version("1.2.3").release
920 (1, 2, 3)
921 >>> Version("2.0.0").release
922 (2, 0, 0)
923 >>> Version("1!2.0.0.post0").release
924 (2, 0, 0)
926 Includes trailing zeroes but not the epoch or any pre-release / development /
927 post-release suffixes.
928 """
929 return self._release
931 @property
932 def pre(self) -> tuple[Literal["a", "b", "rc"], int] | None:
933 """The pre-release segment of the version.
935 >>> print(Version("1.2.3").pre)
936 None
937 >>> Version("1.2.3a1").pre
938 ('a', 1)
939 >>> Version("1.2.3b1").pre
940 ('b', 1)
941 >>> Version("1.2.3rc1").pre
942 ('rc', 1)
943 """
944 return self._pre
946 @property
947 def post(self) -> int | None:
948 """The post-release number of the version.
950 >>> print(Version("1.2.3").post)
951 None
952 >>> Version("1.2.3.post1").post
953 1
954 """
955 return self._post[1] if self._post else None
957 @property
958 def dev(self) -> int | None:
959 """The development number of the version.
961 >>> print(Version("1.2.3").dev)
962 None
963 >>> Version("1.2.3.dev1").dev
964 1
965 """
966 return self._dev[1] if self._dev else None
968 @property
969 def local(self) -> str | None:
970 """The local version segment of the version.
972 >>> print(Version("1.2.3").local)
973 None
974 >>> Version("1.2.3+abc").local
975 'abc'
976 """
977 if self._local:
978 return ".".join(str(x) for x in self._local)
979 else:
980 return None
982 @property
983 def public(self) -> str:
984 """The public portion of the version.
986 This returns a string. If you want a :class:`Version` again and care
987 about performance, use ``v.__replace__(local=None)`` instead.
989 >>> Version("1.2.3").public
990 '1.2.3'
991 >>> Version("1.2.3+abc").public
992 '1.2.3'
993 >>> Version("1!1.2.3dev1+abc").public
994 '1!1.2.3.dev1'
995 """
996 return str(self).split("+", 1)[0]
998 @property
999 def base_version(self) -> str:
1000 """The "base version" of the version.
1002 This returns a string. If you want a :class:`Version` again and care
1003 about performance, use
1004 ``v.__replace__(pre=None, post=None, dev=None, local=None)`` instead.
1006 >>> Version("1.2.3").base_version
1007 '1.2.3'
1008 >>> Version("1.2.3+abc").base_version
1009 '1.2.3'
1010 >>> Version("1!1.2.3dev1+abc").base_version
1011 '1!1.2.3'
1013 The "base version" is the public version of the project without any pre or post
1014 release markers.
1015 """
1016 release_segment = ".".join(map(str, self.release))
1017 return f"{self.epoch}!{release_segment}" if self.epoch else release_segment
1019 @property
1020 def is_prerelease(self) -> bool:
1021 """Whether this version is a pre-release.
1023 >>> Version("1.2.3").is_prerelease
1024 False
1025 >>> Version("1.2.3a1").is_prerelease
1026 True
1027 >>> Version("1.2.3b1").is_prerelease
1028 True
1029 >>> Version("1.2.3rc1").is_prerelease
1030 True
1031 >>> Version("1.2.3dev1").is_prerelease
1032 True
1033 """
1034 return self.dev is not None or self.pre is not None
1036 @property
1037 def is_postrelease(self) -> bool:
1038 """Whether this version is a post-release.
1040 >>> Version("1.2.3").is_postrelease
1041 False
1042 >>> Version("1.2.3.post1").is_postrelease
1043 True
1044 """
1045 return self.post is not None
1047 @property
1048 def is_devrelease(self) -> bool:
1049 """Whether this version is a development release.
1051 >>> Version("1.2.3").is_devrelease
1052 False
1053 >>> Version("1.2.3.dev1").is_devrelease
1054 True
1055 """
1056 return self.dev is not None
1058 @property
1059 def major(self) -> int:
1060 """The first item of :attr:`release` or ``0`` if unavailable.
1062 >>> Version("1.2.3").major
1063 1
1065 .. versionadded:: 20.0
1066 """
1067 return self.release[0] if len(self.release) >= 1 else 0
1069 @property
1070 def minor(self) -> int:
1071 """The second item of :attr:`release` or ``0`` if unavailable.
1073 >>> Version("1.2.3").minor
1074 2
1075 >>> Version("1").minor
1076 0
1078 .. versionadded:: 20.0
1079 """
1080 return self.release[1] if len(self.release) >= 2 else 0
1082 @property
1083 def micro(self) -> int:
1084 """The third item of :attr:`release` or ``0`` if unavailable.
1086 >>> Version("1.2.3").micro
1087 3
1088 >>> Version("1").micro
1089 0
1091 .. versionadded:: 20.0
1092 """
1093 return self.release[2] if len(self.release) >= 3 else 0
1096class _TrimmedRelease(Version):
1097 __slots__ = ()
1099 def __init__(self, version: str | Version) -> None:
1100 if isinstance(version, Version):
1101 self._epoch = version._epoch
1102 self._release = version._release
1103 self._dev = version._dev
1104 self._pre = version._pre
1105 self._post = version._post
1106 self._local = version._local
1107 self._key_cache = version._key_cache
1108 self._hash_cache = version._hash_cache
1109 return
1110 super().__init__(version) # pragma: no cover
1112 @property
1113 def release(self) -> tuple[int, ...]:
1114 """
1115 Release segment without any trailing zeros.
1117 >>> _TrimmedRelease('1.0.0').release
1118 (1,)
1119 >>> _TrimmedRelease('0.0').release
1120 (0,)
1121 """
1122 # This leaves one 0.
1123 rel = super().release
1124 len_release = len(rel)
1125 i = len_release
1126 while i > 1 and rel[i - 1] == 0:
1127 i -= 1
1128 return rel if i == len_release else rel[:i]
1131def _parse_letter_version(
1132 letter: str | None, number: str | bytes | SupportsInt | None
1133) -> tuple[str, int] | None:
1134 if letter:
1135 # We normalize any letters to their lower case form
1136 letter = letter.lower()
1138 # We consider some words to be alternate spellings of other words and
1139 # in those cases we want to normalize the spellings to our preferred
1140 # spelling.
1141 letter = _LETTER_NORMALIZATION.get(letter, letter)
1143 # We consider there to be an implicit 0 in a pre-release if there is
1144 # not a numeral associated with it.
1145 return letter, int(number or 0)
1147 if number:
1148 # We assume if we are given a number, but we are not given a letter
1149 # then this is using the implicit post release syntax (e.g. 1.0-1)
1150 return "post", int(number)
1152 return None
1155_local_version_separators = re.compile(r"[\._-]")
1158def _parse_local_version(local: str | None) -> LocalType | None:
1159 """
1160 Takes a string like ``"abc.1.twelve"`` and turns it into
1161 ``("abc", 1, "twelve")``.
1162 """
1163 if local is not None:
1164 return tuple(
1165 part.lower() if not part.isdigit() else int(part)
1166 for part in _local_version_separators.split(local)
1167 )
1168 return None
1171# Sort ranks for pre-release: dev-only < a < b < rc < stable (no pre-release).
1172_PRE_RANK = {"a": 0, "b": 1, "rc": 2}
1173_PRE_RANK_DEV_ONLY = -1 # sorts before a(0)
1174_PRE_RANK_STABLE = 3 # sorts after rc(2)
1176# In local version segments, strings sort before ints per PEP 440.
1177_LOCAL_STR_RANK = -1 # sorts before all non-negative ints
1179# Pre-computed suffix for stable releases (no pre, post, or dev segments).
1180# See _cmpkey() for the suffix layout.
1181_STABLE_SUFFIX = (_PRE_RANK_STABLE, 0, 0, 0, 1, 0)
1184def _cmpkey(
1185 epoch: int,
1186 release: tuple[int, ...],
1187 pre: tuple[str, int] | None,
1188 post: tuple[str, int] | None,
1189 dev: tuple[str, int] | None,
1190 local: LocalType | None,
1191) -> CmpKey:
1192 """Build a comparison key for PEP 440 ordering.
1194 Returns ``(epoch, release, suffix)`` or
1195 ``(epoch, release, suffix, local)`` so that plain tuple
1196 comparison gives the correct order.
1198 Trailing zeros are stripped from the release so that ``1.0.0 == 1``.
1200 The suffix is a flat 6-int tuple that encodes pre/post/dev:
1201 ``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``
1203 pre_rank: dev-only=-1, a=0, b=1, rc=2, no-pre=3
1204 Dev-only releases (no pre or post) get -1 so they sort before
1205 any alpha/beta/rc. Releases without a pre-release tag get 3
1206 so they sort after rc.
1207 post_rank: no-post=0, post=1
1208 Releases without a post segment sort before those with one.
1209 dev_rank: dev=0, no-dev=1
1210 Releases without a dev segment sort after those with one.
1212 Local segments use ``(n, "")`` for ints and ``(-1, s)`` for strings,
1213 following PEP 440: strings sort before ints, strings compare
1214 lexicographically, ints compare numerically, and shorter segments
1215 sort before longer when prefixes match. Versions without a local
1216 segment sort before those with one (3-tuple < 4-tuple).
1218 >>> _cmpkey(0, (1, 0, 0), None, None, None, None)
1219 (0, (1,), (3, 0, 0, 0, 1, 0))
1220 >>> _cmpkey(0, (1,), ("a", 1), None, None, None)
1221 (0, (1,), (0, 1, 0, 0, 1, 0))
1222 >>> _cmpkey(0, (1,), None, None, None, ("ubuntu", 1))
1223 (0, (1,), (3, 0, 0, 0, 1, 0), ((-1, 'ubuntu'), (1, '')))
1224 """
1225 # Strip trailing zeros: 1.0.0 compares equal to 1.
1226 len_release = len(release)
1227 i = len_release
1228 while i and release[i - 1] == 0:
1229 i -= 1
1230 trimmed = release if i == len_release else release[:i]
1232 # Fast path: stable release with no local segment.
1233 if pre is None and post is None and dev is None and local is None:
1234 return epoch, trimmed, _STABLE_SUFFIX
1236 if pre is None and post is None and dev is not None:
1237 # dev-only (e.g. 1.0.dev1) sorts before all pre-releases.
1238 pre_rank, pre_n = _PRE_RANK_DEV_ONLY, 0
1239 elif pre is None:
1240 pre_rank, pre_n = _PRE_RANK_STABLE, 0
1241 else:
1242 pre_rank, pre_n = _PRE_RANK[pre[0]], pre[1]
1244 post_rank = 0 if post is None else 1
1245 post_n = 0 if post is None else post[1]
1247 dev_rank = 1 if dev is None else 0
1248 dev_n = 0 if dev is None else dev[1]
1250 suffix = (pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)
1252 if local is None:
1253 return epoch, trimmed, suffix
1255 cmp_local: CmpLocalType = tuple(
1256 (seg, "") if isinstance(seg, int) else (_LOCAL_STR_RANK, seg) for seg in local
1257 )
1258 return epoch, trimmed, suffix, cmp_local