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 10import itertools11import re12from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union13 14from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType15 16__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"]17 18LocalType = Tuple[Union[int, str], ...]19 20CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]]21CmpLocalType = Union[22 NegativeInfinityType,23 Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...],24]25CmpKey = Tuple[26 int,27 Tuple[int, ...],28 CmpPrePostDevType,29 CmpPrePostDevType,30 CmpPrePostDevType,31 CmpLocalType,32]33VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]34 35 36class _Version(NamedTuple):37 epoch: int38 release: Tuple[int, ...]39 dev: Optional[Tuple[str, int]]40 pre: Optional[Tuple[str, int]]41 post: Optional[Tuple[str, int]]42 local: Optional[LocalType]43 44 45def parse(version: str) -> "Version":46 """Parse the given version string.47 48 >>> parse('1.0.dev1')49 <Version('1.0.dev1')>50 51 :param version: The version string to parse.52 :raises InvalidVersion: When the version string is not a valid version.53 """54 return Version(version)55 56 57class InvalidVersion(ValueError):58 """Raised when a version string is not a valid version.59 60 >>> Version("invalid")61 Traceback (most recent call last):62 ...63 packaging.version.InvalidVersion: Invalid version: 'invalid'64 """65 66 67class _BaseVersion:68 _key: Tuple[Any, ...]69 70 def __hash__(self) -> int:71 return hash(self._key)72 73 # Please keep the duplicated `isinstance` check74 # in the six comparisons hereunder75 # unless you find a way to avoid adding overhead function calls.76 def __lt__(self, other: "_BaseVersion") -> bool:77 if not isinstance(other, _BaseVersion):78 return NotImplemented79 80 return self._key < other._key81 82 def __le__(self, other: "_BaseVersion") -> bool:83 if not isinstance(other, _BaseVersion):84 return NotImplemented85 86 return self._key <= other._key87 88 def __eq__(self, other: object) -> bool:89 if not isinstance(other, _BaseVersion):90 return NotImplemented91 92 return self._key == other._key93 94 def __ge__(self, other: "_BaseVersion") -> bool:95 if not isinstance(other, _BaseVersion):96 return NotImplemented97 98 return self._key >= other._key99 100 def __gt__(self, other: "_BaseVersion") -> bool:101 if not isinstance(other, _BaseVersion):102 return NotImplemented103 104 return self._key > other._key105 106 def __ne__(self, other: object) -> bool:107 if not isinstance(other, _BaseVersion):108 return NotImplemented109 110 return self._key != other._key111 112 113# Deliberately not anchored to the start and end of the string, to make it114# easier for 3rd party code to reuse115_VERSION_PATTERN = r"""116 v?117 (?:118 (?:(?P<epoch>[0-9]+)!)? # epoch119 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment120 (?P<pre> # pre-release121 [-_\.]?122 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)123 [-_\.]?124 (?P<pre_n>[0-9]+)?125 )?126 (?P<post> # post release127 (?:-(?P<post_n1>[0-9]+))128 |129 (?:130 [-_\.]?131 (?P<post_l>post|rev|r)132 [-_\.]?133 (?P<post_n2>[0-9]+)?134 )135 )?136 (?P<dev> # dev release137 [-_\.]?138 (?P<dev_l>dev)139 [-_\.]?140 (?P<dev_n>[0-9]+)?141 )?142 )143 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version144"""145 146VERSION_PATTERN = _VERSION_PATTERN147"""148A string containing the regular expression used to match a valid version.149 150The pattern is not anchored at either end, and is intended for embedding in larger151expressions (for example, matching a version number as part of a file name). The152regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``153flags set.154 155:meta hide-value:156"""157 158 159class Version(_BaseVersion):160 """This class abstracts handling of a project's versions.161 162 A :class:`Version` instance is comparison aware and can be compared and163 sorted using the standard Python interfaces.164 165 >>> v1 = Version("1.0a5")166 >>> v2 = Version("1.0")167 >>> v1168 <Version('1.0a5')>169 >>> v2170 <Version('1.0')>171 >>> v1 < v2172 True173 >>> v1 == v2174 False175 >>> v1 > v2176 False177 >>> v1 >= v2178 False179 >>> v1 <= v2180 True181 """182 183 _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)184 _key: CmpKey185 186 def __init__(self, version: str) -> None:187 """Initialize a Version object.188 189 :param version:190 The string representation of a version which will be parsed and normalized191 before use.192 :raises InvalidVersion:193 If the ``version`` does not conform to PEP 440 in any way then this194 exception will be raised.195 """196 197 # Validate the version and parse it into pieces198 match = self._regex.search(version)199 if not match:200 raise InvalidVersion(f"Invalid version: '{version}'")201 202 # Store the parsed out pieces of the version203 self._version = _Version(204 epoch=int(match.group("epoch")) if match.group("epoch") else 0,205 release=tuple(int(i) for i in match.group("release").split(".")),206 pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),207 post=_parse_letter_version(208 match.group("post_l"), match.group("post_n1") or match.group("post_n2")209 ),210 dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),211 local=_parse_local_version(match.group("local")),212 )213 214 # Generate a key which will be used for sorting215 self._key = _cmpkey(216 self._version.epoch,217 self._version.release,218 self._version.pre,219 self._version.post,220 self._version.dev,221 self._version.local,222 )223 224 def __repr__(self) -> str:225 """A representation of the Version that shows all internal state.226 227 >>> Version('1.0.0')228 <Version('1.0.0')>229 """230 return f"<Version('{self}')>"231 232 def __str__(self) -> str:233 """A string representation of the version that can be rounded-tripped.234 235 >>> str(Version("1.0a5"))236 '1.0a5'237 """238 parts = []239 240 # Epoch241 if self.epoch != 0:242 parts.append(f"{self.epoch}!")243 244 # Release segment245 parts.append(".".join(str(x) for x in self.release))246 247 # Pre-release248 if self.pre is not None:249 parts.append("".join(str(x) for x in self.pre))250 251 # Post-release252 if self.post is not None:253 parts.append(f".post{self.post}")254 255 # Development release256 if self.dev is not None:257 parts.append(f".dev{self.dev}")258 259 # Local version segment260 if self.local is not None:261 parts.append(f"+{self.local}")262 263 return "".join(parts)264 265 @property266 def epoch(self) -> int:267 """The epoch of the version.268 269 >>> Version("2.0.0").epoch270 0271 >>> Version("1!2.0.0").epoch272 1273 """274 return self._version.epoch275 276 @property277 def release(self) -> Tuple[int, ...]:278 """The components of the "release" segment of the version.279 280 >>> Version("1.2.3").release281 (1, 2, 3)282 >>> Version("2.0.0").release283 (2, 0, 0)284 >>> Version("1!2.0.0.post0").release285 (2, 0, 0)286 287 Includes trailing zeroes but not the epoch or any pre-release / development /288 post-release suffixes.289 """290 return self._version.release291 292 @property293 def pre(self) -> Optional[Tuple[str, int]]:294 """The pre-release segment of the version.295 296 >>> print(Version("1.2.3").pre)297 None298 >>> Version("1.2.3a1").pre299 ('a', 1)300 >>> Version("1.2.3b1").pre301 ('b', 1)302 >>> Version("1.2.3rc1").pre303 ('rc', 1)304 """305 return self._version.pre306 307 @property308 def post(self) -> Optional[int]:309 """The post-release number of the version.310 311 >>> print(Version("1.2.3").post)312 None313 >>> Version("1.2.3.post1").post314 1315 """316 return self._version.post[1] if self._version.post else None317 318 @property319 def dev(self) -> Optional[int]:320 """The development number of the version.321 322 >>> print(Version("1.2.3").dev)323 None324 >>> Version("1.2.3.dev1").dev325 1326 """327 return self._version.dev[1] if self._version.dev else None328 329 @property330 def local(self) -> Optional[str]:331 """The local version segment of the version.332 333 >>> print(Version("1.2.3").local)334 None335 >>> Version("1.2.3+abc").local336 'abc'337 """338 if self._version.local:339 return ".".join(str(x) for x in self._version.local)340 else:341 return None342 343 @property344 def public(self) -> str:345 """The public portion of the version.346 347 >>> Version("1.2.3").public348 '1.2.3'349 >>> Version("1.2.3+abc").public350 '1.2.3'351 >>> Version("1.2.3+abc.dev1").public352 '1.2.3'353 """354 return str(self).split("+", 1)[0]355 356 @property357 def base_version(self) -> str:358 """The "base version" of the version.359 360 >>> Version("1.2.3").base_version361 '1.2.3'362 >>> Version("1.2.3+abc").base_version363 '1.2.3'364 >>> Version("1!1.2.3+abc.dev1").base_version365 '1!1.2.3'366 367 The "base version" is the public version of the project without any pre or post368 release markers.369 """370 parts = []371 372 # Epoch373 if self.epoch != 0:374 parts.append(f"{self.epoch}!")375 376 # Release segment377 parts.append(".".join(str(x) for x in self.release))378 379 return "".join(parts)380 381 @property382 def is_prerelease(self) -> bool:383 """Whether this version is a pre-release.384 385 >>> Version("1.2.3").is_prerelease386 False387 >>> Version("1.2.3a1").is_prerelease388 True389 >>> Version("1.2.3b1").is_prerelease390 True391 >>> Version("1.2.3rc1").is_prerelease392 True393 >>> Version("1.2.3dev1").is_prerelease394 True395 """396 return self.dev is not None or self.pre is not None397 398 @property399 def is_postrelease(self) -> bool:400 """Whether this version is a post-release.401 402 >>> Version("1.2.3").is_postrelease403 False404 >>> Version("1.2.3.post1").is_postrelease405 True406 """407 return self.post is not None408 409 @property410 def is_devrelease(self) -> bool:411 """Whether this version is a development release.412 413 >>> Version("1.2.3").is_devrelease414 False415 >>> Version("1.2.3.dev1").is_devrelease416 True417 """418 return self.dev is not None419 420 @property421 def major(self) -> int:422 """The first item of :attr:`release` or ``0`` if unavailable.423 424 >>> Version("1.2.3").major425 1426 """427 return self.release[0] if len(self.release) >= 1 else 0428 429 @property430 def minor(self) -> int:431 """The second item of :attr:`release` or ``0`` if unavailable.432 433 >>> Version("1.2.3").minor434 2435 >>> Version("1").minor436 0437 """438 return self.release[1] if len(self.release) >= 2 else 0439 440 @property441 def micro(self) -> int:442 """The third item of :attr:`release` or ``0`` if unavailable.443 444 >>> Version("1.2.3").micro445 3446 >>> Version("1").micro447 0448 """449 return self.release[2] if len(self.release) >= 3 else 0450 451 452def _parse_letter_version(453 letter: Optional[str], number: Union[str, bytes, SupportsInt, None]454) -> Optional[Tuple[str, int]]:455 456 if letter:457 # We consider there to be an implicit 0 in a pre-release if there is458 # not a numeral associated with it.459 if number is None:460 number = 0461 462 # We normalize any letters to their lower case form463 letter = letter.lower()464 465 # We consider some words to be alternate spellings of other words and466 # in those cases we want to normalize the spellings to our preferred467 # spelling.468 if letter == "alpha":469 letter = "a"470 elif letter == "beta":471 letter = "b"472 elif letter in ["c", "pre", "preview"]:473 letter = "rc"474 elif letter in ["rev", "r"]:475 letter = "post"476 477 return letter, int(number)478 if not letter and number:479 # We assume if we are given a number, but we are not given a letter480 # then this is using the implicit post release syntax (e.g. 1.0-1)481 letter = "post"482 483 return letter, int(number)484 485 return None486 487 488_local_version_separators = re.compile(r"[\._-]")489 490 491def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:492 """493 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").494 """495 if local is not None:496 return tuple(497 part.lower() if not part.isdigit() else int(part)498 for part in _local_version_separators.split(local)499 )500 return None501 502 503def _cmpkey(504 epoch: int,505 release: Tuple[int, ...],506 pre: Optional[Tuple[str, int]],507 post: Optional[Tuple[str, int]],508 dev: Optional[Tuple[str, int]],509 local: Optional[LocalType],510) -> CmpKey:511 512 # When we compare a release version, we want to compare it with all of the513 # trailing zeros removed. So we'll use a reverse the list, drop all the now514 # leading zeros until we come to something non zero, then take the rest515 # re-reverse it back into the correct order and make it a tuple and use516 # that for our sorting key.517 _release = tuple(518 reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))519 )520 521 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.522 # We'll do this by abusing the pre segment, but we _only_ want to do this523 # if there is not a pre or a post segment. If we have one of those then524 # the normal sorting rules will handle this case correctly.525 if pre is None and post is None and dev is not None:526 _pre: CmpPrePostDevType = NegativeInfinity527 # Versions without a pre-release (except as noted above) should sort after528 # those with one.529 elif pre is None:530 _pre = Infinity531 else:532 _pre = pre533 534 # Versions without a post segment should sort before those with one.535 if post is None:536 _post: CmpPrePostDevType = NegativeInfinity537 538 else:539 _post = post540 541 # Versions without a development segment should sort after those with one.542 if dev is None:543 _dev: CmpPrePostDevType = Infinity544 545 else:546 _dev = dev547 548 if local is None:549 # Versions without a local segment should sort before those with one.550 _local: CmpLocalType = NegativeInfinity551 else:552 # Versions with a local segment need that segment parsed to implement553 # the sorting rules in PEP440.554 # - Alpha numeric segments sort before numeric segments555 # - Alpha numeric segments sort lexicographically556 # - Numeric segments sort numerically557 # - Shorter versions sort before longer versions when the prefixes558 # match exactly559 _local = tuple(560 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local561 )562 563 return epoch, _release, _pre, _post, _dev, _local564 