Aluode/PerceptionLabPortable
0
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 itertools13import re14from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union15 16from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType17 18__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"]19 20LocalType = Tuple[Union[int, str], ...]21 22CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]]23CmpLocalType = Union[24 NegativeInfinityType,25 Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...],26]27CmpKey = Tuple[28 int,29 Tuple[int, ...],30 CmpPrePostDevType,31 CmpPrePostDevType,32 CmpPrePostDevType,33 CmpLocalType,34]35VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]36 37 38class _Version(NamedTuple):39 epoch: int40 release: tuple[int, ...]41 dev: tuple[str, int] | None42 pre: tuple[str, int] | None43 post: tuple[str, int] | None44 local: LocalType | None45 46 47def parse(version: str) -> Version:48 """Parse the given version string.49 50 >>> parse('1.0.dev1')51 <Version('1.0.dev1')>52 53 :param version: The version string to parse.54 :raises InvalidVersion: When the version string is not a valid version.55 """56 return Version(version)57 58 59class InvalidVersion(ValueError):60 """Raised when a version string is not a valid version.61 62 >>> Version("invalid")63 Traceback (most recent call last):64 ...65 packaging.version.InvalidVersion: Invalid version: 'invalid'66 """67 68 69class _BaseVersion:70 _key: tuple[Any, ...]71 72 def __hash__(self) -> int:73 return hash(self._key)74 75 # Please keep the duplicated `isinstance` check76 # in the six comparisons hereunder77 # unless you find a way to avoid adding overhead function calls.78 def __lt__(self, other: _BaseVersion) -> bool:79 if not isinstance(other, _BaseVersion):80 return NotImplemented81 82 return self._key < other._key83 84 def __le__(self, other: _BaseVersion) -> bool:85 if not isinstance(other, _BaseVersion):86 return NotImplemented87 88 return self._key <= other._key89 90 def __eq__(self, other: object) -> bool:91 if not isinstance(other, _BaseVersion):92 return NotImplemented93 94 return self._key == other._key95 96 def __ge__(self, other: _BaseVersion) -> bool:97 if not isinstance(other, _BaseVersion):98 return NotImplemented99 100 return self._key >= other._key101 102 def __gt__(self, other: _BaseVersion) -> bool:103 if not isinstance(other, _BaseVersion):104 return NotImplemented105 106 return self._key > other._key107 108 def __ne__(self, other: object) -> bool:109 if not isinstance(other, _BaseVersion):110 return NotImplemented111 112 return self._key != other._key113 114 115# Deliberately not anchored to the start and end of the string, to make it116# easier for 3rd party code to reuse117_VERSION_PATTERN = r"""118 v?119 (?:120 (?:(?P<epoch>[0-9]+)!)? # epoch121 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment122 (?P<pre> # pre-release123 [-_\.]?124 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)125 [-_\.]?126 (?P<pre_n>[0-9]+)?127 )?128 (?P<post> # post release129 (?:-(?P<post_n1>[0-9]+))130 |131 (?:132 [-_\.]?133 (?P<post_l>post|rev|r)134 [-_\.]?135 (?P<post_n2>[0-9]+)?136 )137 )?138 (?P<dev> # dev release139 [-_\.]?140 (?P<dev_l>dev)141 [-_\.]?142 (?P<dev_n>[0-9]+)?143 )?144 )145 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version146"""147 148VERSION_PATTERN = _VERSION_PATTERN149"""150A string containing the regular expression used to match a valid version.151 152The pattern is not anchored at either end, and is intended for embedding in larger153expressions (for example, matching a version number as part of a file name). The154regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``155flags set.156 157:meta hide-value:158"""159 160 161class Version(_BaseVersion):162 """This class abstracts handling of a project's versions.163 164 A :class:`Version` instance is comparison aware and can be compared and165 sorted using the standard Python interfaces.166 167 >>> v1 = Version("1.0a5")168 >>> v2 = Version("1.0")169 >>> v1170 <Version('1.0a5')>171 >>> v2172 <Version('1.0')>173 >>> v1 < v2174 True175 >>> v1 == v2176 False177 >>> v1 > v2178 False179 >>> v1 >= v2180 False181 >>> v1 <= v2182 True183 """184 185 _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)186 _key: CmpKey187 188 def __init__(self, version: str) -> None:189 """Initialize a Version object.190 191 :param version:192 The string representation of a version which will be parsed and normalized193 before use.194 :raises InvalidVersion:195 If the ``version`` does not conform to PEP 440 in any way then this196 exception will be raised.197 """198 199 # Validate the version and parse it into pieces200 match = self._regex.search(version)201 if not match:202 raise InvalidVersion(f"Invalid version: {version!r}")203 204 # Store the parsed out pieces of the version205 self._version = _Version(206 epoch=int(match.group("epoch")) if match.group("epoch") else 0,207 release=tuple(int(i) for i in match.group("release").split(".")),208 pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),209 post=_parse_letter_version(210 match.group("post_l"), match.group("post_n1") or match.group("post_n2")211 ),212 dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),213 local=_parse_local_version(match.group("local")),214 )215 216 # Generate a key which will be used for sorting217 self._key = _cmpkey(218 self._version.epoch,219 self._version.release,220 self._version.pre,221 self._version.post,222 self._version.dev,223 self._version.local,224 )225 226 def __repr__(self) -> str:227 """A representation of the Version that shows all internal state.228 229 >>> Version('1.0.0')230 <Version('1.0.0')>231 """232 return f"<Version('{self}')>"233 234 def __str__(self) -> str:235 """A string representation of the version that can be round-tripped.236 237 >>> str(Version("1.0a5"))238 '1.0a5'239 """240 parts = []241 242 # Epoch243 if self.epoch != 0:244 parts.append(f"{self.epoch}!")245 246 # Release segment247 parts.append(".".join(str(x) for x in self.release))248 249 # Pre-release250 if self.pre is not None:251 parts.append("".join(str(x) for x in self.pre))252 253 # Post-release254 if self.post is not None:255 parts.append(f".post{self.post}")256 257 # Development release258 if self.dev is not None:259 parts.append(f".dev{self.dev}")260 261 # Local version segment262 if self.local is not None:263 parts.append(f"+{self.local}")264 265 return "".join(parts)266 267 @property268 def epoch(self) -> int:269 """The epoch of the version.270 271 >>> Version("2.0.0").epoch272 0273 >>> Version("1!2.0.0").epoch274 1275 """276 return self._version.epoch277 278 @property279 def release(self) -> tuple[int, ...]:280 """The components of the "release" segment of the version.281 282 >>> Version("1.2.3").release283 (1, 2, 3)284 >>> Version("2.0.0").release285 (2, 0, 0)286 >>> Version("1!2.0.0.post0").release287 (2, 0, 0)288 289 Includes trailing zeroes but not the epoch or any pre-release / development /290 post-release suffixes.291 """292 return self._version.release293 294 @property295 def pre(self) -> tuple[str, int] | None:296 """The pre-release segment of the version.297 298 >>> print(Version("1.2.3").pre)299 None300 >>> Version("1.2.3a1").pre301 ('a', 1)302 >>> Version("1.2.3b1").pre303 ('b', 1)304 >>> Version("1.2.3rc1").pre305 ('rc', 1)306 """307 return self._version.pre308 309 @property310 def post(self) -> int | None:311 """The post-release number of the version.312 313 >>> print(Version("1.2.3").post)314 None315 >>> Version("1.2.3.post1").post316 1317 """318 return self._version.post[1] if self._version.post else None319 320 @property321 def dev(self) -> int | None:322 """The development number of the version.323 324 >>> print(Version("1.2.3").dev)325 None326 >>> Version("1.2.3.dev1").dev327 1328 """329 return self._version.dev[1] if self._version.dev else None330 331 @property332 def local(self) -> str | None:333 """The local version segment of the version.334 335 >>> print(Version("1.2.3").local)336 None337 >>> Version("1.2.3+abc").local338 'abc'339 """340 if self._version.local:341 return ".".join(str(x) for x in self._version.local)342 else:343 return None344 345 @property346 def public(self) -> str:347 """The public portion of the version.348 349 >>> Version("1.2.3").public350 '1.2.3'351 >>> Version("1.2.3+abc").public352 '1.2.3'353 >>> Version("1!1.2.3dev1+abc").public354 '1!1.2.3.dev1'355 """356 return str(self).split("+", 1)[0]357 358 @property359 def base_version(self) -> str:360 """The "base version" of the version.361 362 >>> Version("1.2.3").base_version363 '1.2.3'364 >>> Version("1.2.3+abc").base_version365 '1.2.3'366 >>> Version("1!1.2.3dev1+abc").base_version367 '1!1.2.3'368 369 The "base version" is the public version of the project without any pre or post370 release markers.371 """372 parts = []373 374 # Epoch375 if self.epoch != 0:376 parts.append(f"{self.epoch}!")377 378 # Release segment379 parts.append(".".join(str(x) for x in self.release))380 381 return "".join(parts)382 383 @property384 def is_prerelease(self) -> bool:385 """Whether this version is a pre-release.386 387 >>> Version("1.2.3").is_prerelease388 False389 >>> Version("1.2.3a1").is_prerelease390 True391 >>> Version("1.2.3b1").is_prerelease392 True393 >>> Version("1.2.3rc1").is_prerelease394 True395 >>> Version("1.2.3dev1").is_prerelease396 True397 """398 return self.dev is not None or self.pre is not None399 400 @property401 def is_postrelease(self) -> bool:402 """Whether this version is a post-release.403 404 >>> Version("1.2.3").is_postrelease405 False406 >>> Version("1.2.3.post1").is_postrelease407 True408 """409 return self.post is not None410 411 @property412 def is_devrelease(self) -> bool:413 """Whether this version is a development release.414 415 >>> Version("1.2.3").is_devrelease416 False417 >>> Version("1.2.3.dev1").is_devrelease418 True419 """420 return self.dev is not None421 422 @property423 def major(self) -> int:424 """The first item of :attr:`release` or ``0`` if unavailable.425 426 >>> Version("1.2.3").major427 1428 """429 return self.release[0] if len(self.release) >= 1 else 0430 431 @property432 def minor(self) -> int:433 """The second item of :attr:`release` or ``0`` if unavailable.434 435 >>> Version("1.2.3").minor436 2437 >>> Version("1").minor438 0439 """440 return self.release[1] if len(self.release) >= 2 else 0441 442 @property443 def micro(self) -> int:444 """The third item of :attr:`release` or ``0`` if unavailable.445 446 >>> Version("1.2.3").micro447 3448 >>> Version("1").micro449 0450 """451 return self.release[2] if len(self.release) >= 3 else 0452 453 454class _TrimmedRelease(Version):455 @property456 def release(self) -> tuple[int, ...]:457 """458 Release segment without any trailing zeros.459 460 >>> _TrimmedRelease('1.0.0').release461 (1,)462 >>> _TrimmedRelease('0.0').release463 (0,)464 """465 rel = super().release466 nonzeros = (index for index, val in enumerate(rel) if val)467 last_nonzero = max(nonzeros, default=0)468 return rel[: last_nonzero + 1]469 470 471def _parse_letter_version(472 letter: str | None, number: str | bytes | SupportsInt | None473) -> tuple[str, int] | None:474 if letter:475 # We consider there to be an implicit 0 in a pre-release if there is476 # not a numeral associated with it.477 if number is None:478 number = 0479 480 # We normalize any letters to their lower case form481 letter = letter.lower()482 483 # We consider some words to be alternate spellings of other words and484 # in those cases we want to normalize the spellings to our preferred485 # spelling.486 if letter == "alpha":487 letter = "a"488 elif letter == "beta":489 letter = "b"490 elif letter in ["c", "pre", "preview"]:491 letter = "rc"492 elif letter in ["rev", "r"]:493 letter = "post"494 495 return letter, int(number)496 497 assert not letter498 if number:499 # We assume if we are given a number, but we are not given a letter500 # then this is using the implicit post release syntax (e.g. 1.0-1)501 letter = "post"502 503 return letter, int(number)504 505 return None506 507 508_local_version_separators = re.compile(r"[\._-]")509 510 511def _parse_local_version(local: str | None) -> LocalType | None:512 """513 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").514 """515 if local is not None:516 return tuple(517 part.lower() if not part.isdigit() else int(part)518 for part in _local_version_separators.split(local)519 )520 return None521 522 523def _cmpkey(524 epoch: int,525 release: tuple[int, ...],526 pre: tuple[str, int] | None,527 post: tuple[str, int] | None,528 dev: tuple[str, int] | None,529 local: LocalType | None,530) -> CmpKey:531 # When we compare a release version, we want to compare it with all of the532 # trailing zeros removed. So we'll use a reverse the list, drop all the now533 # leading zeros until we come to something non zero, then take the rest534 # re-reverse it back into the correct order and make it a tuple and use535 # that for our sorting key.536 _release = tuple(537 reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))538 )539 540 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.541 # We'll do this by abusing the pre segment, but we _only_ want to do this542 # if there is not a pre or a post segment. If we have one of those then543 # the normal sorting rules will handle this case correctly.544 if pre is None and post is None and dev is not None:545 _pre: CmpPrePostDevType = NegativeInfinity546 # Versions without a pre-release (except as noted above) should sort after547 # those with one.548 elif pre is None:549 _pre = Infinity550 else:551 _pre = pre552 553 # Versions without a post segment should sort before those with one.554 if post is None:555 _post: CmpPrePostDevType = NegativeInfinity556 557 else:558 _post = post559 560 # Versions without a development segment should sort after those with one.561 if dev is None:562 _dev: CmpPrePostDevType = Infinity563 564 else:565 _dev = dev566 567 if local is None:568 # Versions without a local segment should sort before those with one.569 _local: CmpLocalType = NegativeInfinity570 else:571 # Versions with a local segment need that segment parsed to implement572 # the sorting rules in PEP440.573 # - Alpha numeric segments sort before numeric segments574 # - Alpha numeric segments sort lexicographically575 # - Numeric segments sort numerically576 # - Shorter versions sort before longer versions when the prefixes577 # match exactly578 _local = tuple(579 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local580 )581 582 return epoch, _release, _pre, _post, _dev, _local583 