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
12import re
13import sys
14import typing
15from collections.abc import Callable
16from typing import (
17 Any,
18 Literal,
19 NamedTuple,
20 SupportsInt,
21 TypedDict,
22)
24if typing.TYPE_CHECKING:
25 from typing_extensions import Self, Unpack
27if sys.version_info >= (3, 13): # pragma: no cover
28 from warnings import deprecated as _deprecated
29elif typing.TYPE_CHECKING:
30 from typing_extensions import deprecated as _deprecated
31else: # pragma: no cover
32 import functools
33 import warnings
35 def _deprecated(message: str) -> object:
36 def decorator(func: Callable[[...], object]) -> object:
37 @functools.wraps(func)
38 def wrapper(*args: object, **kwargs: object) -> object:
39 warnings.warn(
40 message,
41 category=DeprecationWarning,
42 stacklevel=2,
43 )
44 return func(*args, **kwargs)
46 return wrapper
48 return decorator
51_LETTER_NORMALIZATION = {
52 "alpha": "a",
53 "beta": "b",
54 "c": "rc",
55 "pre": "rc",
56 "preview": "rc",
57 "rev": "post",
58 "r": "post",
59}
61__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "normalize_pre", "parse"]
64def __dir__() -> list[str]:
65 return __all__
68LocalType = tuple[int | str, ...]
70CmpLocalType = tuple[tuple[int, str], ...]
71CmpSuffix = tuple[int, int, int, int, int, int]
72CmpKey = (
73 tuple[int, tuple[int, ...], CmpSuffix]
74 | tuple[int, tuple[int, ...], CmpSuffix, CmpLocalType]
75)
76VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
79class _VersionReplace(TypedDict, total=False):
80 epoch: int | None
81 release: tuple[int, ...] | None
82 pre: tuple[str, int] | None
83 post: int | None
84 dev: int | None
85 local: str | None
88def normalize_pre(letter: str, /) -> str:
89 """Normalize the pre-release segment of a version string.
91 Returns a lowercase version of the string if not a known pre-release
92 identifier.
94 >>> normalize_pre('alpha')
95 'a'
96 >>> normalize_pre('BETA')
97 'b'
98 >>> normalize_pre('rc')
99 'rc'
101 :param letter:
103 .. versionadded:: 26.1
104 """
105 letter = letter.lower()
106 return _LETTER_NORMALIZATION.get(letter, letter)
109def parse(version: str) -> Version:
110 """Parse the given version string.
112 This is identical to the :class:`Version` constructor.
114 >>> parse('1.0.dev1')
115 <Version('1.0.dev1')>
117 :param version: The version string to parse.
118 :raises InvalidVersion: When the version string is not a valid version.
119 """
120 return Version(version)
123class InvalidVersion(ValueError):
124 """Raised when a version string is not a valid version.
126 >>> Version("invalid")
127 Traceback (most recent call last):
128 ...
129 packaging.version.InvalidVersion: Invalid version: 'invalid'
130 """
133class _BaseVersion:
134 __slots__ = ()
136 # This can also be a normal member (see the packaging_legacy package);
137 # we are just requiring it to be readable. Actually defining a property
138 # has runtime effect on subclasses, so it's typing only.
139 if typing.TYPE_CHECKING:
141 @property
142 def _key(self) -> tuple[Any, ...]: ...
144 def __hash__(self) -> int:
145 return hash(self._key)
147 # Please keep the duplicated `isinstance` check
148 # in the six comparisons hereunder
149 # unless you find a way to avoid adding overhead function calls.
150 def __lt__(self, other: _BaseVersion) -> bool:
151 if not isinstance(other, _BaseVersion):
152 return NotImplemented
154 return self._key < other._key
156 def __le__(self, other: _BaseVersion) -> bool:
157 if not isinstance(other, _BaseVersion):
158 return NotImplemented
160 return self._key <= other._key
162 def __eq__(self, other: object) -> bool:
163 if not isinstance(other, _BaseVersion):
164 return NotImplemented
166 return self._key == other._key
168 def __ge__(self, other: _BaseVersion) -> bool:
169 if not isinstance(other, _BaseVersion):
170 return NotImplemented
172 return self._key >= other._key
174 def __gt__(self, other: _BaseVersion) -> bool:
175 if not isinstance(other, _BaseVersion):
176 return NotImplemented
178 return self._key > other._key
180 def __ne__(self, other: object) -> bool:
181 if not isinstance(other, _BaseVersion):
182 return NotImplemented
184 return self._key != other._key
187# Deliberately not anchored to the start and end of the string, to make it
188# easier for 3rd party code to reuse
190# Note that ++ doesn't behave identically on CPython and PyPy, so not using it here
191_VERSION_PATTERN = r"""
192 v?+ # optional leading v
193 (?a:
194 (?:(?P<epoch>[0-9]+)!)?+ # epoch
195 (?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment
196 (?P<pre> # pre-release
197 [._-]?+
198 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
199 [._-]?+
200 (?P<pre_n>[0-9]+)?
201 )?+
202 (?P<post> # post release
203 (?:-(?P<post_n1>[0-9]+))
204 |
205 (?:
206 [._-]?
207 (?P<post_l>post|rev|r)
208 [._-]?
209 (?P<post_n2>[0-9]+)?
210 )
211 )?+
212 (?P<dev> # dev release
213 [._-]?+
214 (?P<dev_l>dev)
215 [._-]?+
216 (?P<dev_n>[0-9]+)?
217 )?+
218 )
219 (?a:\+
220 (?P<local> # local version
221 [a-z0-9]+
222 (?:[._-][a-z0-9]+)*+
223 )
224 )?+
225"""
227_VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?")
229# Possessive qualifiers were added in Python 3.11.
230# CPython 3.11.0-3.11.4 had a bug: https://github.com/python/cpython/pull/107795
231# Older PyPy also had a bug.
232VERSION_PATTERN = (
233 _VERSION_PATTERN_OLD
234 if (sys.implementation.name == "cpython" and sys.version_info < (3, 11, 5))
235 or (sys.implementation.name == "pypy" and sys.version_info < (3, 11, 13))
236 or sys.version_info < (3, 11)
237 else _VERSION_PATTERN
238)
239"""
240A string containing the regular expression used to match a valid version.
242The pattern is not anchored at either end, and is intended for embedding in larger
243expressions (for example, matching a version number as part of a file name). The
244regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
245flags set.
247.. versionchanged:: 26.0
249 The regex now uses possessive qualifiers on Python 3.11 if they are
250 supported (CPython 3.11.5+, PyPy 3.11.13+).
252:meta hide-value:
253"""
256# Validation pattern for local version in replace()
257_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE | re.ASCII)
259# Fast path: If a version has only digits and dots then we
260# can skip the regex and parse it as a release segment
261_SIMPLE_VERSION_INDICATORS = frozenset(".0123456789")
264def _validate_epoch(value: object, /) -> int:
265 epoch = value or 0
266 if isinstance(epoch, int) and epoch >= 0:
267 return epoch
268 msg = f"epoch must be non-negative integer, got {epoch}"
269 raise InvalidVersion(msg)
272def _validate_release(value: object, /) -> tuple[int, ...]:
273 release = (0,) if value is None else value
274 if (
275 isinstance(release, tuple)
276 and len(release) > 0
277 and all(isinstance(i, int) and i >= 0 for i in release)
278 ):
279 return release
280 msg = f"release must be a non-empty tuple of non-negative integers, got {release}"
281 raise InvalidVersion(msg)
284def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None:
285 if value is None:
286 return value
287 if isinstance(value, tuple) and len(value) == 2:
288 letter, number = value
289 # The letter must be a string before it can be normalized.
290 if (
291 isinstance(letter, str)
292 and (normalized := normalize_pre(letter)) in {"a", "b", "rc"}
293 and isinstance(number, int)
294 and number >= 0
295 ):
296 # type checkers can't infer the Literal type here on letter
297 return (normalized, number) # type: ignore[return-value]
298 msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}"
299 raise InvalidVersion(msg)
302def _validate_post(value: object, /) -> tuple[Literal["post"], int] | None:
303 if value is None:
304 return value
305 if isinstance(value, int) and value >= 0:
306 return ("post", value)
307 msg = f"post must be non-negative integer, got {value}"
308 raise InvalidVersion(msg)
311def _validate_dev(value: object, /) -> tuple[Literal["dev"], int] | None:
312 if value is None:
313 return value
314 if isinstance(value, int) and value >= 0:
315 return ("dev", value)
316 msg = f"dev must be non-negative integer, got {value}"
317 raise InvalidVersion(msg)
320def _validate_local(value: object, /) -> LocalType | None:
321 if value is None:
322 return value
323 if isinstance(value, str) and _LOCAL_PATTERN.fullmatch(value):
324 return _parse_local_version(value)
325 msg = f"local must be a valid version string, got {value!r}"
326 raise InvalidVersion(msg)
329# Backward compatibility for internals before 26.0. Do not use.
330class _Version(NamedTuple):
331 epoch: int
332 release: tuple[int, ...]
333 dev: tuple[Literal["dev"], int] | None
334 pre: tuple[Literal["a", "b", "rc"], int] | None
335 post: tuple[Literal["post"], int] | None
336 local: LocalType | None
339class Version(_BaseVersion):
340 """This class abstracts handling of a project's versions.
342 A :class:`Version` instance is comparison aware and can be compared and
343 sorted using the standard Python interfaces.
345 >>> v1 = Version("1.0a5")
346 >>> v2 = Version("1.0")
347 >>> v1
348 <Version('1.0a5')>
349 >>> v2
350 <Version('1.0')>
351 >>> v1 < v2
352 True
353 >>> v1 == v2
354 False
355 >>> v1 > v2
356 False
357 >>> v1 >= v2
358 False
359 >>> v1 <= v2
360 True
362 :class:`Version` is immutable; use :meth:`__replace__` to change
363 part of a version.
365 Instances are safe to serialize with :mod:`pickle`. They use a stable
366 format so the same pickle can be loaded in future packaging releases.
368 .. versionchanged:: 26.2
370 Added a stable pickle format. Pickles created with packaging 26.2+ can
371 be unpickled with future releases. Backward compatibility with pickles
372 from packaging < 26.2 is supported but may be removed in a future
373 release.
374 """
376 __slots__ = (
377 "_dev",
378 "_epoch",
379 "_hash_cache",
380 "_key_cache",
381 "_local",
382 "_post",
383 "_pre",
384 "_release",
385 )
386 __match_args__ = ("_str",)
387 """
388 Pattern matching is supported on Python 3.10+.
390 .. versionadded:: 26.0
392 :meta hide-value:
393 """
395 _regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE)
397 _epoch: int
398 _release: tuple[int, ...]
399 _dev: tuple[Literal["dev"], int] | None
400 _pre: tuple[Literal["a", "b", "rc"], int] | None
401 _post: tuple[Literal["post"], int] | None
402 _local: LocalType | None
404 _hash_cache: int | None
405 _key_cache: CmpKey | None
407 def __init__(self, version: str) -> None:
408 """Initialize a Version object.
410 :param version:
411 The string representation of a version which will be parsed and normalized
412 before use.
413 :raises InvalidVersion:
414 If the ``version`` does not conform to PEP 440 in any way then this
415 exception will be raised.
416 """
417 try:
418 is_simple = _SIMPLE_VERSION_INDICATORS.issuperset(version)
419 except TypeError:
420 raise InvalidVersion(f"Invalid version: {version!r}") from None
422 if is_simple:
423 try:
424 self._release = tuple(map(int, version.split(".")))
425 except AttributeError:
426 raise InvalidVersion(f"Invalid version: {version!r}") from None
427 except ValueError:
428 # Empty parts (from "1..2", ".1", etc.) are invalid versions.
429 # Any other ValueError (e.g. int str-digits limit) should
430 # propagate to the caller.
431 if "" in version.split("."):
432 raise InvalidVersion(f"Invalid version: {version!r}") from None
433 raise
435 self._epoch = 0
436 self._pre = None
437 self._post = None
438 self._dev = None
439 self._local = None
440 self._key_cache = None
441 self._hash_cache = None
442 return
444 # Validate the version and parse it into pieces
445 try:
446 match = self._regex.fullmatch(version)
447 except TypeError:
448 raise InvalidVersion(f"Invalid version: {version!r}") from None
449 if not match:
450 raise InvalidVersion(f"Invalid version: {version!r}")
451 self._epoch = int(match.group("epoch")) if match.group("epoch") else 0
452 self._release = tuple(map(int, match.group("release").split(".")))
453 # We can type ignore the assignments below because the regex guarantees
454 # the correct strings
455 self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n")) # type: ignore[assignment]
456 self._post = _parse_letter_version( # type: ignore[assignment]
457 match.group("post_l"), match.group("post_n1") or match.group("post_n2")
458 )
459 self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n")) # type: ignore[assignment]
460 self._local = _parse_local_version(match.group("local"))
462 # Key which will be used for sorting
463 self._key_cache = None
464 self._hash_cache = None
466 @classmethod
467 def from_parts(
468 cls,
469 *,
470 epoch: int = 0,
471 release: tuple[int, ...],
472 pre: tuple[str, int] | None = None,
473 post: int | None = None,
474 dev: int | None = None,
475 local: str | None = None,
476 ) -> Self:
477 """
478 Return a new version composed of the various parts.
480 This allows you to build a version without going though a string and
481 running a regular expression. It normalizes pre-release strings. The
482 ``release=`` keyword argument is required.
484 >>> Version.from_parts(release=(1,2,3))
485 <Version('1.2.3')>
486 >>> Version.from_parts(release=(0,1,0), pre=("b", 1))
487 <Version('0.1.0b1')>
489 :param epoch:
490 :param release: This version tuple is required
492 .. versionadded:: 26.1
493 """
494 _epoch = _validate_epoch(epoch)
495 _release = _validate_release(release)
496 _pre = _validate_pre(pre) if pre is not None else None
497 _post = _validate_post(post) if post is not None else None
498 _dev = _validate_dev(dev) if dev is not None else None
499 _local = _validate_local(local) if local is not None else None
501 new_version = cls.__new__(cls)
502 new_version._key_cache = None
503 new_version._hash_cache = None
504 new_version._epoch = _epoch
505 new_version._release = _release
506 new_version._pre = _pre
507 new_version._post = _post
508 new_version._dev = _dev
509 new_version._local = _local
511 return new_version
513 def __replace__(self, **kwargs: Unpack[_VersionReplace]) -> Self:
514 """
515 __replace__(*, epoch=..., release=..., pre=..., post=..., dev=..., local=...)
517 Return a new version with parts replaced.
519 This returns a new version (unless no parts were changed). The
520 pre-release is normalized. Setting a value to ``None`` clears it.
522 >>> v = Version("1.2.3")
523 >>> v.__replace__(pre=("a", 1))
524 <Version('1.2.3a1')>
526 :param int | None epoch:
527 :param tuple[int, ...] | None release:
528 :param tuple[str, int] | None pre:
529 :param int | None post:
530 :param int | None dev:
531 :param str | None local:
533 .. versionadded:: 26.0
534 .. versionchanged:: 26.1
536 The pre-release portion is now normalized.
537 """
538 epoch = _validate_epoch(kwargs["epoch"]) if "epoch" in kwargs else self._epoch
539 release = (
540 _validate_release(kwargs["release"])
541 if "release" in kwargs
542 else self._release
543 )
544 pre = _validate_pre(kwargs["pre"]) if "pre" in kwargs else self._pre
545 post = _validate_post(kwargs["post"]) if "post" in kwargs else self._post
546 dev = _validate_dev(kwargs["dev"]) if "dev" in kwargs else self._dev
547 local = _validate_local(kwargs["local"]) if "local" in kwargs else self._local
549 if (
550 epoch == self._epoch
551 and release == self._release
552 and pre == self._pre
553 and post == self._post
554 and dev == self._dev
555 and local == self._local
556 ):
557 return self
559 new_version = self.__class__.__new__(self.__class__)
560 new_version._key_cache = None
561 new_version._hash_cache = None
562 new_version._epoch = epoch
563 new_version._release = release
564 new_version._pre = pre
565 new_version._post = post
566 new_version._dev = dev
567 new_version._local = local
569 return new_version
571 @property
572 def _key(self) -> CmpKey:
573 if self._key_cache is None:
574 self._key_cache = _cmpkey(
575 self._epoch,
576 self._release,
577 self._pre,
578 self._post,
579 self._dev,
580 self._local,
581 )
582 return self._key_cache
584 # __hash__ must be defined when __eq__ is overridden,
585 # otherwise Python sets __hash__ to None.
586 def __hash__(self) -> int:
587 if (cached_hash := self._hash_cache) is not None:
588 return cached_hash
590 if (key := self._key_cache) is None:
591 self._key_cache = key = _cmpkey(
592 self._epoch,
593 self._release,
594 self._pre,
595 self._post,
596 self._dev,
597 self._local,
598 )
599 self._hash_cache = cached_hash = hash(key)
600 return cached_hash
602 # Override comparison methods to use direct _key_cache access
603 # This is faster than property access, especially before Python 3.12
604 def __lt__(self, other: _BaseVersion) -> bool:
605 if isinstance(other, Version):
606 if self._key_cache is None:
607 self._key_cache = _cmpkey(
608 self._epoch,
609 self._release,
610 self._pre,
611 self._post,
612 self._dev,
613 self._local,
614 )
615 if other._key_cache is None:
616 other._key_cache = _cmpkey(
617 other._epoch,
618 other._release,
619 other._pre,
620 other._post,
621 other._dev,
622 other._local,
623 )
624 return self._key_cache < other._key_cache
626 if not isinstance(other, _BaseVersion):
627 return NotImplemented
629 return super().__lt__(other)
631 def __le__(self, other: _BaseVersion) -> bool:
632 if isinstance(other, Version):
633 if self._key_cache is None:
634 self._key_cache = _cmpkey(
635 self._epoch,
636 self._release,
637 self._pre,
638 self._post,
639 self._dev,
640 self._local,
641 )
642 if other._key_cache is None:
643 other._key_cache = _cmpkey(
644 other._epoch,
645 other._release,
646 other._pre,
647 other._post,
648 other._dev,
649 other._local,
650 )
651 return self._key_cache <= other._key_cache
653 if not isinstance(other, _BaseVersion):
654 return NotImplemented
656 return super().__le__(other)
658 def __eq__(self, other: object) -> bool:
659 if isinstance(other, Version):
660 if self._key_cache is None:
661 self._key_cache = _cmpkey(
662 self._epoch,
663 self._release,
664 self._pre,
665 self._post,
666 self._dev,
667 self._local,
668 )
669 if other._key_cache is None:
670 other._key_cache = _cmpkey(
671 other._epoch,
672 other._release,
673 other._pre,
674 other._post,
675 other._dev,
676 other._local,
677 )
678 return self._key_cache == other._key_cache
680 if not isinstance(other, _BaseVersion):
681 return NotImplemented
683 return super().__eq__(other)
685 def __ge__(self, other: _BaseVersion) -> bool:
686 if isinstance(other, Version):
687 if self._key_cache is None:
688 self._key_cache = _cmpkey(
689 self._epoch,
690 self._release,
691 self._pre,
692 self._post,
693 self._dev,
694 self._local,
695 )
696 if other._key_cache is None:
697 other._key_cache = _cmpkey(
698 other._epoch,
699 other._release,
700 other._pre,
701 other._post,
702 other._dev,
703 other._local,
704 )
705 return self._key_cache >= other._key_cache
707 if not isinstance(other, _BaseVersion):
708 return NotImplemented
710 return super().__ge__(other)
712 def __gt__(self, other: _BaseVersion) -> bool:
713 if isinstance(other, Version):
714 if self._key_cache is None:
715 self._key_cache = _cmpkey(
716 self._epoch,
717 self._release,
718 self._pre,
719 self._post,
720 self._dev,
721 self._local,
722 )
723 if other._key_cache is None:
724 other._key_cache = _cmpkey(
725 other._epoch,
726 other._release,
727 other._pre,
728 other._post,
729 other._dev,
730 other._local,
731 )
732 return self._key_cache > other._key_cache
734 if not isinstance(other, _BaseVersion):
735 return NotImplemented
737 return super().__gt__(other)
739 def __ne__(self, other: object) -> bool:
740 if isinstance(other, Version):
741 if self._key_cache is None:
742 self._key_cache = _cmpkey(
743 self._epoch,
744 self._release,
745 self._pre,
746 self._post,
747 self._dev,
748 self._local,
749 )
750 if other._key_cache is None:
751 other._key_cache = _cmpkey(
752 other._epoch,
753 other._release,
754 other._pre,
755 other._post,
756 other._dev,
757 other._local,
758 )
759 return self._key_cache != other._key_cache
761 if not isinstance(other, _BaseVersion):
762 return NotImplemented
764 return super().__ne__(other)
766 def __getstate__(
767 self,
768 ) -> tuple[
769 int,
770 tuple[int, ...],
771 tuple[str, int] | None,
772 tuple[str, int] | None,
773 tuple[str, int] | None,
774 LocalType | None,
775 ]:
776 # Return state as a 6-item tuple for compactness:
777 # (epoch, release, pre, post, dev, local)
778 # Cache members are excluded and will be recomputed on demand
779 return (
780 self._epoch,
781 self._release,
782 self._pre,
783 self._post,
784 self._dev,
785 self._local,
786 )
788 def __setstate__(self, state: object) -> None:
789 # Always discard cached values — they may contain stale references
790 # (e.g. packaging._structures.InfinityType from pre-26.1 pickles)
791 # and will be recomputed on demand from the core fields above.
792 self._key_cache = None
793 self._hash_cache = None
795 if isinstance(state, tuple):
796 if len(state) == 6:
797 # New format (26.2+): (epoch, release, pre, post, dev, local)
798 (
799 self._epoch,
800 self._release,
801 self._pre,
802 self._post,
803 self._dev,
804 self._local,
805 ) = state
806 return
807 if len(state) == 2:
808 # Format (packaging 26.0-26.1): (None, {slot: value}).
809 _, slot_dict = state
810 if isinstance(slot_dict, dict):
811 self._epoch = slot_dict["_epoch"]
812 self._release = slot_dict["_release"]
813 self._pre = slot_dict.get("_pre")
814 self._post = slot_dict.get("_post")
815 self._dev = slot_dict.get("_dev")
816 self._local = slot_dict.get("_local")
817 return
818 if isinstance(state, dict):
819 # Old format (packaging <= 25.x, no __slots__): state is a plain
820 # dict with "_version" (_Version NamedTuple) and "_key" entries.
821 version_nt = state.get("_version")
822 if version_nt is not None:
823 self._epoch = version_nt.epoch
824 self._release = version_nt.release
825 self._pre = version_nt.pre
826 self._post = version_nt.post
827 self._dev = version_nt.dev
828 self._local = version_nt.local
829 return
831 raise TypeError(f"Cannot restore Version from {state!r}")
833 @property
834 @_deprecated("Version._version is private and will be removed soon")
835 def _version(self) -> _Version:
836 return _Version(
837 self._epoch, self._release, self._dev, self._pre, self._post, self._local
838 )
840 @_version.setter
841 @_deprecated("Version._version is private and will be removed soon")
842 def _version(self, value: _Version) -> None:
843 self._epoch = value.epoch
844 self._release = value.release
845 self._dev = value.dev
846 self._pre = value.pre
847 self._post = value.post
848 self._local = value.local
849 self._key_cache = None
850 self._hash_cache = None
852 def __repr__(self) -> str:
853 """A representation of the Version that shows all internal state.
855 >>> Version('1.0.0')
856 <Version('1.0.0')>
857 """
858 return f"<{self.__class__.__name__}({str(self)!r})>"
860 def __str__(self) -> str:
861 """A string representation of the version that can be round-tripped.
863 >>> str(Version("1.0a5"))
864 '1.0a5'
865 """
866 # This is a hot function, so not calling self.base_version
867 version = ".".join(map(str, self.release))
869 # Epoch
870 if self.epoch:
871 version = f"{self.epoch}!{version}"
873 # Pre-release
874 if self.pre is not None:
875 version += "".join(map(str, self.pre))
877 # Post-release
878 if self.post is not None:
879 version += f".post{self.post}"
881 # Development release
882 if self.dev is not None:
883 version += f".dev{self.dev}"
885 # Local version segment
886 if self.local is not None:
887 version += f"+{self.local}"
889 return version
891 @property
892 def _str(self) -> str:
893 """Internal property for match_args"""
894 return str(self)
896 @property
897 def epoch(self) -> int:
898 """The epoch of the version.
900 >>> Version("2.0.0").epoch
901 0
902 >>> Version("1!2.0.0").epoch
903 1
904 """
905 return self._epoch
907 @property
908 def release(self) -> tuple[int, ...]:
909 """The components of the "release" segment of the version.
911 >>> Version("1.2.3").release
912 (1, 2, 3)
913 >>> Version("2.0.0").release
914 (2, 0, 0)
915 >>> Version("1!2.0.0.post0").release
916 (2, 0, 0)
918 Includes trailing zeroes but not the epoch or any pre-release / development /
919 post-release suffixes.
920 """
921 return self._release
923 @property
924 def pre(self) -> tuple[Literal["a", "b", "rc"], int] | None:
925 """The pre-release segment of the version.
927 >>> print(Version("1.2.3").pre)
928 None
929 >>> Version("1.2.3a1").pre
930 ('a', 1)
931 >>> Version("1.2.3b1").pre
932 ('b', 1)
933 >>> Version("1.2.3rc1").pre
934 ('rc', 1)
935 """
936 return self._pre
938 @property
939 def post(self) -> int | None:
940 """The post-release number of the version.
942 >>> print(Version("1.2.3").post)
943 None
944 >>> Version("1.2.3.post1").post
945 1
946 """
947 return self._post[1] if self._post else None
949 @property
950 def dev(self) -> int | None:
951 """The development number of the version.
953 >>> print(Version("1.2.3").dev)
954 None
955 >>> Version("1.2.3.dev1").dev
956 1
957 """
958 return self._dev[1] if self._dev else None
960 @property
961 def local(self) -> str | None:
962 """The local version segment of the version.
964 >>> print(Version("1.2.3").local)
965 None
966 >>> Version("1.2.3+abc").local
967 'abc'
968 """
969 if self._local:
970 return ".".join(str(x) for x in self._local)
971 else:
972 return None
974 @property
975 def public(self) -> str:
976 """The public portion of the version.
978 This returns a string. If you want a :class:`Version` again and care
979 about performance, use ``v.__replace__(local=None)`` instead.
981 >>> Version("1.2.3").public
982 '1.2.3'
983 >>> Version("1.2.3+abc").public
984 '1.2.3'
985 >>> Version("1!1.2.3dev1+abc").public
986 '1!1.2.3.dev1'
987 """
988 return str(self).split("+", 1)[0]
990 @property
991 def base_version(self) -> str:
992 """The "base version" of the version.
994 This returns a string. If you want a :class:`Version` again and care
995 about performance, use
996 ``v.__replace__(pre=None, post=None, dev=None, local=None)`` instead.
998 >>> Version("1.2.3").base_version
999 '1.2.3'
1000 >>> Version("1.2.3+abc").base_version
1001 '1.2.3'
1002 >>> Version("1!1.2.3dev1+abc").base_version
1003 '1!1.2.3'
1005 The "base version" is the public version of the project without any pre or post
1006 release markers.
1007 """
1008 release_segment = ".".join(map(str, self.release))
1009 return f"{self.epoch}!{release_segment}" if self.epoch else release_segment
1011 @property
1012 def is_prerelease(self) -> bool:
1013 """Whether this version is a pre-release.
1015 >>> Version("1.2.3").is_prerelease
1016 False
1017 >>> Version("1.2.3a1").is_prerelease
1018 True
1019 >>> Version("1.2.3b1").is_prerelease
1020 True
1021 >>> Version("1.2.3rc1").is_prerelease
1022 True
1023 >>> Version("1.2.3dev1").is_prerelease
1024 True
1025 """
1026 return self.dev is not None or self.pre is not None
1028 @property
1029 def is_postrelease(self) -> bool:
1030 """Whether this version is a post-release.
1032 >>> Version("1.2.3").is_postrelease
1033 False
1034 >>> Version("1.2.3.post1").is_postrelease
1035 True
1036 """
1037 return self.post is not None
1039 @property
1040 def is_devrelease(self) -> bool:
1041 """Whether this version is a development release.
1043 >>> Version("1.2.3").is_devrelease
1044 False
1045 >>> Version("1.2.3.dev1").is_devrelease
1046 True
1047 """
1048 return self.dev is not None
1050 @property
1051 def major(self) -> int:
1052 """The first item of :attr:`release` or ``0`` if unavailable.
1054 >>> Version("1.2.3").major
1055 1
1057 .. versionadded:: 20.0
1058 """
1059 return self.release[0] if len(self.release) >= 1 else 0
1061 @property
1062 def minor(self) -> int:
1063 """The second item of :attr:`release` or ``0`` if unavailable.
1065 >>> Version("1.2.3").minor
1066 2
1067 >>> Version("1").minor
1068 0
1070 .. versionadded:: 20.0
1071 """
1072 return self.release[1] if len(self.release) >= 2 else 0
1074 @property
1075 def micro(self) -> int:
1076 """The third item of :attr:`release` or ``0`` if unavailable.
1078 >>> Version("1.2.3").micro
1079 3
1080 >>> Version("1").micro
1081 0
1083 .. versionadded:: 20.0
1084 """
1085 return self.release[2] if len(self.release) >= 3 else 0
1088class _TrimmedRelease(Version):
1089 __slots__ = ()
1091 def __init__(self, version: str | Version) -> None:
1092 if isinstance(version, Version):
1093 self._epoch = version._epoch
1094 self._release = version._release
1095 self._dev = version._dev
1096 self._pre = version._pre
1097 self._post = version._post
1098 self._local = version._local
1099 self._key_cache = version._key_cache
1100 self._hash_cache = version._hash_cache
1101 return
1102 super().__init__(version) # pragma: no cover
1104 @property
1105 def release(self) -> tuple[int, ...]:
1106 """
1107 Release segment without any trailing zeros.
1109 >>> _TrimmedRelease('1.0.0').release
1110 (1,)
1111 >>> _TrimmedRelease('0.0').release
1112 (0,)
1113 """
1114 # This leaves one 0.
1115 rel = super().release
1116 len_release = len(rel)
1117 i = len_release
1118 while i > 1 and rel[i - 1] == 0:
1119 i -= 1
1120 return rel if i == len_release else rel[:i]
1123def _parse_letter_version(
1124 letter: str | None, number: str | bytes | SupportsInt | None
1125) -> tuple[str, int] | None:
1126 if letter:
1127 # We normalize any letters to their lower case form
1128 letter = letter.lower()
1130 # We consider some words to be alternate spellings of other words and
1131 # in those cases we want to normalize the spellings to our preferred
1132 # spelling.
1133 letter = _LETTER_NORMALIZATION.get(letter, letter)
1135 # We consider there to be an implicit 0 in a pre-release if there is
1136 # not a numeral associated with it.
1137 return letter, int(number or 0)
1139 if number:
1140 # We assume if we are given a number, but we are not given a letter
1141 # then this is using the implicit post release syntax (e.g. 1.0-1)
1142 return "post", int(number)
1144 return None
1147_local_version_separators = re.compile(r"[\._-]")
1150def _parse_local_version(local: str | None) -> LocalType | None:
1151 """
1152 Takes a string like ``"abc.1.twelve"`` and turns it into
1153 ``("abc", 1, "twelve")``.
1154 """
1155 if local is not None:
1156 return tuple(
1157 part.lower() if not part.isdigit() else int(part)
1158 for part in _local_version_separators.split(local)
1159 )
1160 return None
1163# Sort ranks for pre-release: dev-only < a < b < rc < stable (no pre-release).
1164_PRE_RANK = {"a": 0, "b": 1, "rc": 2}
1165_PRE_RANK_DEV_ONLY = -1 # sorts before a(0)
1166_PRE_RANK_STABLE = 3 # sorts after rc(2)
1168# In local version segments, strings sort before ints per PEP 440.
1169_LOCAL_STR_RANK = -1 # sorts before all non-negative ints
1171# Pre-computed suffix for stable releases (no pre, post, or dev segments).
1172# See _cmpkey() for the suffix layout.
1173_STABLE_SUFFIX = (_PRE_RANK_STABLE, 0, 0, 0, 1, 0)
1176def _cmpkey(
1177 epoch: int,
1178 release: tuple[int, ...],
1179 pre: tuple[str, int] | None,
1180 post: tuple[str, int] | None,
1181 dev: tuple[str, int] | None,
1182 local: LocalType | None,
1183) -> CmpKey:
1184 """Build a comparison key for PEP 440 ordering.
1186 Returns ``(epoch, release, suffix)`` or
1187 ``(epoch, release, suffix, local)`` so that plain tuple
1188 comparison gives the correct order.
1190 Trailing zeros are stripped from the release so that ``1.0.0 == 1``.
1192 The suffix is a flat 6-int tuple that encodes pre/post/dev:
1193 ``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``
1195 pre_rank: dev-only=-1, a=0, b=1, rc=2, no-pre=3
1196 Dev-only releases (no pre or post) get -1 so they sort before
1197 any alpha/beta/rc. Releases without a pre-release tag get 3
1198 so they sort after rc.
1199 post_rank: no-post=0, post=1
1200 Releases without a post segment sort before those with one.
1201 dev_rank: dev=0, no-dev=1
1202 Releases without a dev segment sort after those with one.
1204 Local segments use ``(n, "")`` for ints and ``(-1, s)`` for strings,
1205 following PEP 440: strings sort before ints, strings compare
1206 lexicographically, ints compare numerically, and shorter segments
1207 sort before longer when prefixes match. Versions without a local
1208 segment sort before those with one (3-tuple < 4-tuple).
1210 >>> _cmpkey(0, (1, 0, 0), None, None, None, None)
1211 (0, (1,), (3, 0, 0, 0, 1, 0))
1212 >>> _cmpkey(0, (1,), ("a", 1), None, None, None)
1213 (0, (1,), (0, 1, 0, 0, 1, 0))
1214 >>> _cmpkey(0, (1,), None, None, None, ("ubuntu", 1))
1215 (0, (1,), (3, 0, 0, 0, 1, 0), ((-1, 'ubuntu'), (1, '')))
1216 """
1217 # Strip trailing zeros: 1.0.0 compares equal to 1.
1218 len_release = len(release)
1219 i = len_release
1220 while i and release[i - 1] == 0:
1221 i -= 1
1222 trimmed = release if i == len_release else release[:i]
1224 # Fast path: stable release with no local segment.
1225 if pre is None and post is None and dev is None and local is None:
1226 return epoch, trimmed, _STABLE_SUFFIX
1228 if pre is None and post is None and dev is not None:
1229 # dev-only (e.g. 1.0.dev1) sorts before all pre-releases.
1230 pre_rank, pre_n = _PRE_RANK_DEV_ONLY, 0
1231 elif pre is None:
1232 pre_rank, pre_n = _PRE_RANK_STABLE, 0
1233 else:
1234 pre_rank, pre_n = _PRE_RANK[pre[0]], pre[1]
1236 post_rank = 0 if post is None else 1
1237 post_n = 0 if post is None else post[1]
1239 dev_rank = 1 if dev is None else 0
1240 dev_n = 0 if dev is None else dev[1]
1242 suffix = (pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)
1244 if local is None:
1245 return epoch, trimmed, suffix
1247 cmp_local: CmpLocalType = tuple(
1248 (seg, "") if isinstance(seg, int) else (_LOCAL_STR_RANK, seg) for seg in local
1249 )
1250 return epoch, trimmed, suffix, cmp_local