version.py (16676B)
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:: 6 7 from packaging.version import parse, Version 8 """ 9 10 from __future__ import annotations 11 12 import itertools 13 import re 14 from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union 15 16 from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType 17 18 __all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"] 19 20 LocalType = Tuple[Union[int, str], ...] 21 22 CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] 23 CmpLocalType = Union[ 24 NegativeInfinityType, 25 Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], 26 ] 27 CmpKey = Tuple[ 28 int, 29 Tuple[int, ...], 30 CmpPrePostDevType, 31 CmpPrePostDevType, 32 CmpPrePostDevType, 33 CmpLocalType, 34 ] 35 VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] 36 37 38 class _Version(NamedTuple): 39 epoch: int 40 release: tuple[int, ...] 41 dev: tuple[str, int] | None 42 pre: tuple[str, int] | None 43 post: tuple[str, int] | None 44 local: LocalType | None 45 46 47 def 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 59 class 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 69 class _BaseVersion: 70 _key: tuple[Any, ...] 71 72 def __hash__(self) -> int: 73 return hash(self._key) 74 75 # Please keep the duplicated `isinstance` check 76 # in the six comparisons hereunder 77 # 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 NotImplemented 81 82 return self._key < other._key 83 84 def __le__(self, other: _BaseVersion) -> bool: 85 if not isinstance(other, _BaseVersion): 86 return NotImplemented 87 88 return self._key <= other._key 89 90 def __eq__(self, other: object) -> bool: 91 if not isinstance(other, _BaseVersion): 92 return NotImplemented 93 94 return self._key == other._key 95 96 def __ge__(self, other: _BaseVersion) -> bool: 97 if not isinstance(other, _BaseVersion): 98 return NotImplemented 99 100 return self._key >= other._key 101 102 def __gt__(self, other: _BaseVersion) -> bool: 103 if not isinstance(other, _BaseVersion): 104 return NotImplemented 105 106 return self._key > other._key 107 108 def __ne__(self, other: object) -> bool: 109 if not isinstance(other, _BaseVersion): 110 return NotImplemented 111 112 return self._key != other._key 113 114 115 # Deliberately not anchored to the start and end of the string, to make it 116 # easier for 3rd party code to reuse 117 _VERSION_PATTERN = r""" 118 v? 119 (?: 120 (?:(?P<epoch>[0-9]+)!)? # epoch 121 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment 122 (?P<pre> # pre-release 123 [-_\.]? 124 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc) 125 [-_\.]? 126 (?P<pre_n>[0-9]+)? 127 )? 128 (?P<post> # post release 129 (?:-(?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 release 139 [-_\.]? 140 (?P<dev_l>dev) 141 [-_\.]? 142 (?P<dev_n>[0-9]+)? 143 )? 144 ) 145 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version 146 """ 147 148 VERSION_PATTERN = _VERSION_PATTERN 149 """ 150 A string containing the regular expression used to match a valid version. 151 152 The pattern is not anchored at either end, and is intended for embedding in larger 153 expressions (for example, matching a version number as part of a file name). The 154 regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE`` 155 flags set. 156 157 :meta hide-value: 158 """ 159 160 161 class 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 and 165 sorted using the standard Python interfaces. 166 167 >>> v1 = Version("1.0a5") 168 >>> v2 = Version("1.0") 169 >>> v1 170 <Version('1.0a5')> 171 >>> v2 172 <Version('1.0')> 173 >>> v1 < v2 174 True 175 >>> v1 == v2 176 False 177 >>> v1 > v2 178 False 179 >>> v1 >= v2 180 False 181 >>> v1 <= v2 182 True 183 """ 184 185 _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) 186 _key: CmpKey 187 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 normalized 193 before use. 194 :raises InvalidVersion: 195 If the ``version`` does not conform to PEP 440 in any way then this 196 exception will be raised. 197 """ 198 199 # Validate the version and parse it into pieces 200 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 version 205 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 sorting 217 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 # Epoch 243 if self.epoch != 0: 244 parts.append(f"{self.epoch}!") 245 246 # Release segment 247 parts.append(".".join(str(x) for x in self.release)) 248 249 # Pre-release 250 if self.pre is not None: 251 parts.append("".join(str(x) for x in self.pre)) 252 253 # Post-release 254 if self.post is not None: 255 parts.append(f".post{self.post}") 256 257 # Development release 258 if self.dev is not None: 259 parts.append(f".dev{self.dev}") 260 261 # Local version segment 262 if self.local is not None: 263 parts.append(f"+{self.local}") 264 265 return "".join(parts) 266 267 @property 268 def epoch(self) -> int: 269 """The epoch of the version. 270 271 >>> Version("2.0.0").epoch 272 0 273 >>> Version("1!2.0.0").epoch 274 1 275 """ 276 return self._version.epoch 277 278 @property 279 def release(self) -> tuple[int, ...]: 280 """The components of the "release" segment of the version. 281 282 >>> Version("1.2.3").release 283 (1, 2, 3) 284 >>> Version("2.0.0").release 285 (2, 0, 0) 286 >>> Version("1!2.0.0.post0").release 287 (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.release 293 294 @property 295 def pre(self) -> tuple[str, int] | None: 296 """The pre-release segment of the version. 297 298 >>> print(Version("1.2.3").pre) 299 None 300 >>> Version("1.2.3a1").pre 301 ('a', 1) 302 >>> Version("1.2.3b1").pre 303 ('b', 1) 304 >>> Version("1.2.3rc1").pre 305 ('rc', 1) 306 """ 307 return self._version.pre 308 309 @property 310 def post(self) -> int | None: 311 """The post-release number of the version. 312 313 >>> print(Version("1.2.3").post) 314 None 315 >>> Version("1.2.3.post1").post 316 1 317 """ 318 return self._version.post[1] if self._version.post else None 319 320 @property 321 def dev(self) -> int | None: 322 """The development number of the version. 323 324 >>> print(Version("1.2.3").dev) 325 None 326 >>> Version("1.2.3.dev1").dev 327 1 328 """ 329 return self._version.dev[1] if self._version.dev else None 330 331 @property 332 def local(self) -> str | None: 333 """The local version segment of the version. 334 335 >>> print(Version("1.2.3").local) 336 None 337 >>> Version("1.2.3+abc").local 338 'abc' 339 """ 340 if self._version.local: 341 return ".".join(str(x) for x in self._version.local) 342 else: 343 return None 344 345 @property 346 def public(self) -> str: 347 """The public portion of the version. 348 349 >>> Version("1.2.3").public 350 '1.2.3' 351 >>> Version("1.2.3+abc").public 352 '1.2.3' 353 >>> Version("1!1.2.3dev1+abc").public 354 '1!1.2.3.dev1' 355 """ 356 return str(self).split("+", 1)[0] 357 358 @property 359 def base_version(self) -> str: 360 """The "base version" of the version. 361 362 >>> Version("1.2.3").base_version 363 '1.2.3' 364 >>> Version("1.2.3+abc").base_version 365 '1.2.3' 366 >>> Version("1!1.2.3dev1+abc").base_version 367 '1!1.2.3' 368 369 The "base version" is the public version of the project without any pre or post 370 release markers. 371 """ 372 parts = [] 373 374 # Epoch 375 if self.epoch != 0: 376 parts.append(f"{self.epoch}!") 377 378 # Release segment 379 parts.append(".".join(str(x) for x in self.release)) 380 381 return "".join(parts) 382 383 @property 384 def is_prerelease(self) -> bool: 385 """Whether this version is a pre-release. 386 387 >>> Version("1.2.3").is_prerelease 388 False 389 >>> Version("1.2.3a1").is_prerelease 390 True 391 >>> Version("1.2.3b1").is_prerelease 392 True 393 >>> Version("1.2.3rc1").is_prerelease 394 True 395 >>> Version("1.2.3dev1").is_prerelease 396 True 397 """ 398 return self.dev is not None or self.pre is not None 399 400 @property 401 def is_postrelease(self) -> bool: 402 """Whether this version is a post-release. 403 404 >>> Version("1.2.3").is_postrelease 405 False 406 >>> Version("1.2.3.post1").is_postrelease 407 True 408 """ 409 return self.post is not None 410 411 @property 412 def is_devrelease(self) -> bool: 413 """Whether this version is a development release. 414 415 >>> Version("1.2.3").is_devrelease 416 False 417 >>> Version("1.2.3.dev1").is_devrelease 418 True 419 """ 420 return self.dev is not None 421 422 @property 423 def major(self) -> int: 424 """The first item of :attr:`release` or ``0`` if unavailable. 425 426 >>> Version("1.2.3").major 427 1 428 """ 429 return self.release[0] if len(self.release) >= 1 else 0 430 431 @property 432 def minor(self) -> int: 433 """The second item of :attr:`release` or ``0`` if unavailable. 434 435 >>> Version("1.2.3").minor 436 2 437 >>> Version("1").minor 438 0 439 """ 440 return self.release[1] if len(self.release) >= 2 else 0 441 442 @property 443 def micro(self) -> int: 444 """The third item of :attr:`release` or ``0`` if unavailable. 445 446 >>> Version("1.2.3").micro 447 3 448 >>> Version("1").micro 449 0 450 """ 451 return self.release[2] if len(self.release) >= 3 else 0 452 453 454 class _TrimmedRelease(Version): 455 @property 456 def release(self) -> tuple[int, ...]: 457 """ 458 Release segment without any trailing zeros. 459 460 >>> _TrimmedRelease('1.0.0').release 461 (1,) 462 >>> _TrimmedRelease('0.0').release 463 (0,) 464 """ 465 rel = super().release 466 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 471 def _parse_letter_version( 472 letter: str | None, number: str | bytes | SupportsInt | None 473 ) -> tuple[str, int] | None: 474 if letter: 475 # We consider there to be an implicit 0 in a pre-release if there is 476 # not a numeral associated with it. 477 if number is None: 478 number = 0 479 480 # We normalize any letters to their lower case form 481 letter = letter.lower() 482 483 # We consider some words to be alternate spellings of other words and 484 # in those cases we want to normalize the spellings to our preferred 485 # 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 letter 498 if number: 499 # We assume if we are given a number, but we are not given a letter 500 # 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 None 506 507 508 _local_version_separators = re.compile(r"[\._-]") 509 510 511 def _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 None 521 522 523 def _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 the 532 # trailing zeros removed. So we'll use a reverse the list, drop all the now 533 # leading zeros until we come to something non zero, then take the rest 534 # re-reverse it back into the correct order and make it a tuple and use 535 # 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 this 542 # if there is not a pre or a post segment. If we have one of those then 543 # 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 = NegativeInfinity 546 # Versions without a pre-release (except as noted above) should sort after 547 # those with one. 548 elif pre is None: 549 _pre = Infinity 550 else: 551 _pre = pre 552 553 # Versions without a post segment should sort before those with one. 554 if post is None: 555 _post: CmpPrePostDevType = NegativeInfinity 556 557 else: 558 _post = post 559 560 # Versions without a development segment should sort after those with one. 561 if dev is None: 562 _dev: CmpPrePostDevType = Infinity 563 564 else: 565 _dev = dev 566 567 if local is None: 568 # Versions without a local segment should sort before those with one. 569 _local: CmpLocalType = NegativeInfinity 570 else: 571 # Versions with a local segment need that segment parsed to implement 572 # the sorting rules in PEP440. 573 # - Alpha numeric segments sort before numeric segments 574 # - Alpha numeric segments sort lexicographically 575 # - Numeric segments sort numerically 576 # - Shorter versions sort before longer versions when the prefixes 577 # match exactly 578 _local = tuple( 579 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local 580 ) 581 582 return epoch, _release, _pre, _post, _dev, _local