codekingpro/portable-devtools
114k
1# This file is dual licensed under the terms of the Apache License, Version2# 2.0, and the BSD License. See the LICENSE file in the root of this repository3# for complete details.4"""5.. testsetup::6 7 from packaging.version import parse, Version8"""9 10from __future__ import annotations11 12import re13import sys14import typing15from typing import (16 Any,17 Callable,18 Literal,19 NamedTuple,20 SupportsInt,21 Tuple,22 TypedDict,23 Union,24)25 26from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType27 28if typing.TYPE_CHECKING:29 from typing_extensions import Self, Unpack30 31if sys.version_info >= (3, 13): # pragma: no cover32 from warnings import deprecated as _deprecated33elif typing.TYPE_CHECKING:34 from typing_extensions import deprecated as _deprecated35else: # pragma: no cover36 import functools37 import warnings38 39 def _deprecated(message: str) -> object:40 def decorator(func: 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)49 50 return wrapper51 52 return decorator53 54 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}64 65__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"]66 67LocalType = Tuple[Union[int, str], ...]68 69CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]]70CmpLocalType = Union[71 NegativeInfinityType,72 Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...],73]74CmpKey = Tuple[75 int,76 Tuple[int, ...],77 CmpPrePostDevType,78 CmpPrePostDevType,79 CmpPrePostDevType,80 CmpLocalType,81]82VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]83 84 85class _VersionReplace(TypedDict, total=False):86 epoch: int | None87 release: tuple[int, ...] | None88 pre: tuple[Literal["a", "b", "rc"], int] | None89 post: int | None90 dev: int | None91 local: str | None92 93 94def parse(version: str) -> Version:95 """Parse the given version string.96 97 >>> parse('1.0.dev1')98 <Version('1.0.dev1')>99 100 :param version: The version string to parse.101 :raises InvalidVersion: When the version string is not a valid version.102 """103 return Version(version)104 105 106class InvalidVersion(ValueError):107 """Raised when a version string is not a valid version.108 109 >>> Version("invalid")110 Traceback (most recent call last):111 ...112 packaging.version.InvalidVersion: Invalid version: 'invalid'113 """114 115 116class _BaseVersion:117 __slots__ = ()118 119 # This can also be a normal member (see the packaging_legacy package);120 # we are just requiring it to be readable. Actually defining a property121 # has runtime effect on subclasses, so it's typing only.122 if typing.TYPE_CHECKING:123 124 @property125 def _key(self) -> tuple[Any, ...]: ...126 127 def __hash__(self) -> int:128 return hash(self._key)129 130 # Please keep the duplicated `isinstance` check131 # in the six comparisons hereunder132 # unless you find a way to avoid adding overhead function calls.133 def __lt__(self, other: _BaseVersion) -> bool:134 if not isinstance(other, _BaseVersion):135 return NotImplemented136 137 return self._key < other._key138 139 def __le__(self, other: _BaseVersion) -> bool:140 if not isinstance(other, _BaseVersion):141 return NotImplemented142 143 return self._key <= other._key144 145 def __eq__(self, other: object) -> bool:146 if not isinstance(other, _BaseVersion):147 return NotImplemented148 149 return self._key == other._key150 151 def __ge__(self, other: _BaseVersion) -> bool:152 if not isinstance(other, _BaseVersion):153 return NotImplemented154 155 return self._key >= other._key156 157 def __gt__(self, other: _BaseVersion) -> bool:158 if not isinstance(other, _BaseVersion):159 return NotImplemented160 161 return self._key > other._key162 163 def __ne__(self, other: object) -> bool:164 if not isinstance(other, _BaseVersion):165 return NotImplemented166 167 return self._key != other._key168 169 170# Deliberately not anchored to the start and end of the string, to make it171# easier for 3rd party code to reuse172 173# Note that ++ doesn't behave identically on CPython and PyPy, so not using it here174_VERSION_PATTERN = r"""175 v?+ # optional leading v176 (?:177 (?:(?P<epoch>[0-9]+)!)?+ # epoch178 (?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment179 (?P<pre> # pre-release180 [._-]?+181 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)182 [._-]?+183 (?P<pre_n>[0-9]+)?184 )?+185 (?P<post> # post release186 (?:-(?P<post_n1>[0-9]+))187 |188 (?:189 [._-]?190 (?P<post_l>post|rev|r)191 [._-]?192 (?P<post_n2>[0-9]+)?193 )194 )?+195 (?P<dev> # dev release196 [._-]?+197 (?P<dev_l>dev)198 [._-]?+199 (?P<dev_n>[0-9]+)?200 )?+201 )202 (?:\+203 (?P<local> # local version204 [a-z0-9]+205 (?:[._-][a-z0-9]+)*+206 )207 )?+208"""209 210_VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?")211 212# Possessive qualifiers were added in Python 3.11.213# CPython 3.11.0-3.11.4 had a bug: https://github.com/python/cpython/pull/107795214# Older PyPy also had a bug.215VERSION_PATTERN = (216 _VERSION_PATTERN_OLD217 if (sys.implementation.name == "cpython" and sys.version_info < (3, 11, 5))218 or (sys.implementation.name == "pypy" and sys.version_info < (3, 11, 13))219 or sys.version_info < (3, 11)220 else _VERSION_PATTERN221)222"""223A string containing the regular expression used to match a valid version.224 225The pattern is not anchored at either end, and is intended for embedding in larger226expressions (for example, matching a version number as part of a file name). The227regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``228flags set.229 230:meta hide-value:231"""232 233 234# Validation pattern for local version in replace()235_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE)236 237 238def _validate_epoch(value: object, /) -> int:239 epoch = value or 0240 if isinstance(epoch, int) and epoch >= 0:241 return epoch242 msg = f"epoch must be non-negative integer, got {epoch}"243 raise InvalidVersion(msg)244 245 246def _validate_release(value: object, /) -> tuple[int, ...]:247 release = (0,) if value is None else value248 if (249 isinstance(release, tuple)250 and len(release) > 0251 and all(isinstance(i, int) and i >= 0 for i in release)252 ):253 return release254 msg = f"release must be a non-empty tuple of non-negative integers, got {release}"255 raise InvalidVersion(msg)256 257 258def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None:259 if value is None:260 return value261 if (262 isinstance(value, tuple)263 and len(value) == 2264 and value[0] in ("a", "b", "rc")265 and isinstance(value[1], int)266 and value[1] >= 0267 ):268 return value269 msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}"270 raise InvalidVersion(msg)271 272 273def _validate_post(value: object, /) -> tuple[Literal["post"], int] | None:274 if value is None:275 return value276 if isinstance(value, int) and value >= 0:277 return ("post", value)278 msg = f"post must be non-negative integer, got {value}"279 raise InvalidVersion(msg)280 281 282def _validate_dev(value: object, /) -> tuple[Literal["dev"], int] | None:283 if value is None:284 return value285 if isinstance(value, int) and value >= 0:286 return ("dev", value)287 msg = f"dev must be non-negative integer, got {value}"288 raise InvalidVersion(msg)289 290 291def _validate_local(value: object, /) -> LocalType | None:292 if value is None:293 return value294 if isinstance(value, str) and _LOCAL_PATTERN.fullmatch(value):295 return _parse_local_version(value)296 msg = f"local must be a valid version string, got {value!r}"297 raise InvalidVersion(msg)298 299 300# Backward compatibility for internals before 26.0. Do not use.301class _Version(NamedTuple):302 epoch: int303 release: tuple[int, ...]304 dev: tuple[str, int] | None305 pre: tuple[str, int] | None306 post: tuple[str, int] | None307 local: LocalType | None308 309 310class Version(_BaseVersion):311 """This class abstracts handling of a project's versions.312 313 A :class:`Version` instance is comparison aware and can be compared and314 sorted using the standard Python interfaces.315 316 >>> v1 = Version("1.0a5")317 >>> v2 = Version("1.0")318 >>> v1319 <Version('1.0a5')>320 >>> v2321 <Version('1.0')>322 >>> v1 < v2323 True324 >>> v1 == v2325 False326 >>> v1 > v2327 False328 >>> v1 >= v2329 False330 >>> v1 <= v2331 True332 """333 334 __slots__ = ("_dev", "_epoch", "_key_cache", "_local", "_post", "_pre", "_release")335 __match_args__ = ("_str",)336 337 _regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE)338 339 _epoch: int340 _release: tuple[int, ...]341 _dev: tuple[str, int] | None342 _pre: tuple[str, int] | None343 _post: tuple[str, int] | None344 _local: LocalType | None345 346 _key_cache: CmpKey | None347 348 def __init__(self, version: str) -> None:349 """Initialize a Version object.350 351 :param version:352 The string representation of a version which will be parsed and normalized353 before use.354 :raises InvalidVersion:355 If the ``version`` does not conform to PEP 440 in any way then this356 exception will be raised.357 """358 # Validate the version and parse it into pieces359 match = self._regex.fullmatch(version)360 if not match:361 raise InvalidVersion(f"Invalid version: {version!r}")362 self._epoch = int(match.group("epoch")) if match.group("epoch") else 0363 self._release = tuple(map(int, match.group("release").split(".")))364 self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n"))365 self._post = _parse_letter_version(366 match.group("post_l"), match.group("post_n1") or match.group("post_n2")367 )368 self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n"))369 self._local = _parse_local_version(match.group("local"))370 371 # Key which will be used for sorting372 self._key_cache = None373 374 def __replace__(self, **kwargs: Unpack[_VersionReplace]) -> Self:375 epoch = _validate_epoch(kwargs["epoch"]) if "epoch" in kwargs else self._epoch376 release = (377 _validate_release(kwargs["release"])378 if "release" in kwargs379 else self._release380 )381 pre = _validate_pre(kwargs["pre"]) if "pre" in kwargs else self._pre382 post = _validate_post(kwargs["post"]) if "post" in kwargs else self._post383 dev = _validate_dev(kwargs["dev"]) if "dev" in kwargs else self._dev384 local = _validate_local(kwargs["local"]) if "local" in kwargs else self._local385 386 if (387 epoch == self._epoch388 and release == self._release389 and pre == self._pre390 and post == self._post391 and dev == self._dev392 and local == self._local393 ):394 return self395 396 new_version = self.__class__.__new__(self.__class__)397 new_version._key_cache = None398 new_version._epoch = epoch399 new_version._release = release400 new_version._pre = pre401 new_version._post = post402 new_version._dev = dev403 new_version._local = local404 405 return new_version406 407 @property408 def _key(self) -> CmpKey:409 if self._key_cache is None:410 self._key_cache = _cmpkey(411 self._epoch,412 self._release,413 self._pre,414 self._post,415 self._dev,416 self._local,417 )418 return self._key_cache419 420 @property421 @_deprecated("Version._version is private and will be removed soon")422 def _version(self) -> _Version:423 return _Version(424 self._epoch, self._release, self._dev, self._pre, self._post, self._local425 )426 427 @_version.setter428 @_deprecated("Version._version is private and will be removed soon")429 def _version(self, value: _Version) -> None:430 self._epoch = value.epoch431 self._release = value.release432 self._dev = value.dev433 self._pre = value.pre434 self._post = value.post435 self._local = value.local436 self._key_cache = None437 438 def __repr__(self) -> str:439 """A representation of the Version that shows all internal state.440 441 >>> Version('1.0.0')442 <Version('1.0.0')>443 """444 return f"<Version('{self}')>"445 446 def __str__(self) -> str:447 """A string representation of the version that can be round-tripped.448 449 >>> str(Version("1.0a5"))450 '1.0a5'451 """452 # This is a hot function, so not calling self.base_version453 version = ".".join(map(str, self.release))454 455 # Epoch456 if self.epoch:457 version = f"{self.epoch}!{version}"458 459 # Pre-release460 if self.pre is not None:461 version += "".join(map(str, self.pre))462 463 # Post-release464 if self.post is not None:465 version += f".post{self.post}"466 467 # Development release468 if self.dev is not None:469 version += f".dev{self.dev}"470 471 # Local version segment472 if self.local is not None:473 version += f"+{self.local}"474 475 return version476 477 @property478 def _str(self) -> str:479 """Internal property for match_args"""480 return str(self)481 482 @property483 def epoch(self) -> int:484 """The epoch of the version.485 486 >>> Version("2.0.0").epoch487 0488 >>> Version("1!2.0.0").epoch489 1490 """491 return self._epoch492 493 @property494 def release(self) -> tuple[int, ...]:495 """The components of the "release" segment of the version.496 497 >>> Version("1.2.3").release498 (1, 2, 3)499 >>> Version("2.0.0").release500 (2, 0, 0)501 >>> Version("1!2.0.0.post0").release502 (2, 0, 0)503 504 Includes trailing zeroes but not the epoch or any pre-release / development /505 post-release suffixes.506 """507 return self._release508 509 @property510 def pre(self) -> tuple[str, int] | None:511 """The pre-release segment of the version.512 513 >>> print(Version("1.2.3").pre)514 None515 >>> Version("1.2.3a1").pre516 ('a', 1)517 >>> Version("1.2.3b1").pre518 ('b', 1)519 >>> Version("1.2.3rc1").pre520 ('rc', 1)521 """522 return self._pre523 524 @property525 def post(self) -> int | None:526 """The post-release number of the version.527 528 >>> print(Version("1.2.3").post)529 None530 >>> Version("1.2.3.post1").post531 1532 """533 return self._post[1] if self._post else None534 535 @property536 def dev(self) -> int | None:537 """The development number of the version.538 539 >>> print(Version("1.2.3").dev)540 None541 >>> Version("1.2.3.dev1").dev542 1543 """544 return self._dev[1] if self._dev else None545 546 @property547 def local(self) -> str | None:548 """The local version segment of the version.549 550 >>> print(Version("1.2.3").local)551 None552 >>> Version("1.2.3+abc").local553 'abc'554 """555 if self._local:556 return ".".join(str(x) for x in self._local)557 else:558 return None559 560 @property561 def public(self) -> str:562 """The public portion of the version.563 564 >>> Version("1.2.3").public565 '1.2.3'566 >>> Version("1.2.3+abc").public567 '1.2.3'568 >>> Version("1!1.2.3dev1+abc").public569 '1!1.2.3.dev1'570 """571 return str(self).split("+", 1)[0]572 573 @property574 def base_version(self) -> str:575 """The "base version" of the version.576 577 >>> Version("1.2.3").base_version578 '1.2.3'579 >>> Version("1.2.3+abc").base_version580 '1.2.3'581 >>> Version("1!1.2.3dev1+abc").base_version582 '1!1.2.3'583 584 The "base version" is the public version of the project without any pre or post585 release markers.586 """587 release_segment = ".".join(map(str, self.release))588 return f"{self.epoch}!{release_segment}" if self.epoch else release_segment589 590 @property591 def is_prerelease(self) -> bool:592 """Whether this version is a pre-release.593 594 >>> Version("1.2.3").is_prerelease595 False596 >>> Version("1.2.3a1").is_prerelease597 True598 >>> Version("1.2.3b1").is_prerelease599 True600 >>> Version("1.2.3rc1").is_prerelease601 True602 >>> Version("1.2.3dev1").is_prerelease603 True604 """605 return self.dev is not None or self.pre is not None606 607 @property608 def is_postrelease(self) -> bool:609 """Whether this version is a post-release.610 611 >>> Version("1.2.3").is_postrelease612 False613 >>> Version("1.2.3.post1").is_postrelease614 True615 """616 return self.post is not None617 618 @property619 def is_devrelease(self) -> bool:620 """Whether this version is a development release.621 622 >>> Version("1.2.3").is_devrelease623 False624 >>> Version("1.2.3.dev1").is_devrelease625 True626 """627 return self.dev is not None628 629 @property630 def major(self) -> int:631 """The first item of :attr:`release` or ``0`` if unavailable.632 633 >>> Version("1.2.3").major634 1635 """636 return self.release[0] if len(self.release) >= 1 else 0637 638 @property639 def minor(self) -> int:640 """The second item of :attr:`release` or ``0`` if unavailable.641 642 >>> Version("1.2.3").minor643 2644 >>> Version("1").minor645 0646 """647 return self.release[1] if len(self.release) >= 2 else 0648 649 @property650 def micro(self) -> int:651 """The third item of :attr:`release` or ``0`` if unavailable.652 653 >>> Version("1.2.3").micro654 3655 >>> Version("1").micro656 0657 """658 return self.release[2] if len(self.release) >= 3 else 0659 660 661class _TrimmedRelease(Version):662 __slots__ = ()663 664 def __init__(self, version: str | Version) -> None:665 if isinstance(version, Version):666 self._epoch = version._epoch667 self._release = version._release668 self._dev = version._dev669 self._pre = version._pre670 self._post = version._post671 self._local = version._local672 self._key_cache = version._key_cache673 return674 super().__init__(version) # pragma: no cover675 676 @property677 def release(self) -> tuple[int, ...]:678 """679 Release segment without any trailing zeros.680 681 >>> _TrimmedRelease('1.0.0').release682 (1,)683 >>> _TrimmedRelease('0.0').release684 (0,)685 """686 # This leaves one 0.687 rel = super().release688 len_release = len(rel)689 i = len_release690 while i > 1 and rel[i - 1] == 0:691 i -= 1692 return rel if i == len_release else rel[:i]693 694 695def _parse_letter_version(696 letter: str | None, number: str | bytes | SupportsInt | None697) -> tuple[str, int] | None:698 if letter:699 # We normalize any letters to their lower case form700 letter = letter.lower()701 702 # We consider some words to be alternate spellings of other words and703 # in those cases we want to normalize the spellings to our preferred704 # spelling.705 letter = _LETTER_NORMALIZATION.get(letter, letter)706 707 # We consider there to be an implicit 0 in a pre-release if there is708 # not a numeral associated with it.709 return letter, int(number or 0)710 711 if number:712 # We assume if we are given a number, but we are not given a letter713 # then this is using the implicit post release syntax (e.g. 1.0-1)714 return "post", int(number)715 716 return None717 718 719_local_version_separators = re.compile(r"[\._-]")720 721 722def _parse_local_version(local: str | None) -> LocalType | None:723 """724 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").725 """726 if local is not None:727 return tuple(728 part.lower() if not part.isdigit() else int(part)729 for part in _local_version_separators.split(local)730 )731 return None732 733 734def _cmpkey(735 epoch: int,736 release: tuple[int, ...],737 pre: tuple[str, int] | None,738 post: tuple[str, int] | None,739 dev: tuple[str, int] | None,740 local: LocalType | None,741) -> CmpKey:742 # When we compare a release version, we want to compare it with all of the743 # trailing zeros removed. We will use this for our sorting key.744 len_release = len(release)745 i = len_release746 while i and release[i - 1] == 0:747 i -= 1748 _release = release if i == len_release else release[:i]749 750 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.751 # We'll do this by abusing the pre segment, but we _only_ want to do this752 # if there is not a pre or a post segment. If we have one of those then753 # the normal sorting rules will handle this case correctly.754 if pre is None and post is None and dev is not None:755 _pre: CmpPrePostDevType = NegativeInfinity756 # Versions without a pre-release (except as noted above) should sort after757 # those with one.758 elif pre is None:759 _pre = Infinity760 else:761 _pre = pre762 763 # Versions without a post segment should sort before those with one.764 if post is None:765 _post: CmpPrePostDevType = NegativeInfinity766 767 else:768 _post = post769 770 # Versions without a development segment should sort after those with one.771 if dev is None:772 _dev: CmpPrePostDevType = Infinity773 774 else:775 _dev = dev776 777 if local is None:778 # Versions without a local segment should sort before those with one.779 _local: CmpLocalType = NegativeInfinity780 else:781 # Versions with a local segment need that segment parsed to implement782 # the sorting rules in PEP440.783 # - Alpha numeric segments sort before numeric segments784 # - Alpha numeric segments sort lexicographically785 # - Numeric segments sort numerically786 # - Shorter versions sort before longer versions when the prefixes787 # match exactly788 _local = tuple(789 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local790 )791 792 return epoch, _release, _pre, _post, _dev, _local793 