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 5import collections 6import itertools 7import re 8import warnings 9from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union 10 11from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType 12 13__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] 14 15InfiniteTypes = Union[InfinityType, NegativeInfinityType] 16PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] 17SubLocalType = Union[InfiniteTypes, int, str] 18LocalType = Union[ 19 NegativeInfinityType, 20 Tuple[ 21 Union[ 22 SubLocalType, 23 Tuple[SubLocalType, str], 24 Tuple[NegativeInfinityType, SubLocalType], 25 ], 26 ..., 27 ], 28] 29CmpKey = Tuple[ 30 int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType 31] 32LegacyCmpKey = Tuple[int, Tuple[str, ...]] 33VersionComparisonMethod = Callable[ 34 [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool 35] 36 37_Version = collections.namedtuple( 38 "_Version", ["epoch", "release", "dev", "pre", "post", "local"] 39) 40 41 42def parse(version: str) -> Union["LegacyVersion", "Version"]: 43 """ 44 Parse the given version string and return either a :class:`Version` object 45 or a :class:`LegacyVersion` object depending on if the given version is 46 a valid PEP 440 version or a legacy version. 47 """ 48 try: 49 return Version(version) 50 except InvalidVersion: 51 return LegacyVersion(version) 52 53 54class InvalidVersion(ValueError): 55 """ 56 An invalid version was found, users should refer to PEP 440. 57 """ 58 59 60class _BaseVersion: 61 _key: Union[CmpKey, LegacyCmpKey] 62 63 def __hash__(self) -> int: 64 return hash(self._key) 65 66 # Please keep the duplicated `isinstance` check 67 # in the six comparisons hereunder 68 # unless you find a way to avoid adding overhead function calls. 69 def __lt__(self, other: "_BaseVersion") -> bool: 70 if not isinstance(other, _BaseVersion): 71 return NotImplemented 72 73 return self._key < other._key 74 75 def __le__(self, other: "_BaseVersion") -> bool: 76 if not isinstance(other, _BaseVersion): 77 return NotImplemented 78 79 return self._key <= other._key 80 81 def __eq__(self, other: object) -> bool: 82 if not isinstance(other, _BaseVersion): 83 return NotImplemented 84 85 return self._key == other._key 86 87 def __ge__(self, other: "_BaseVersion") -> bool: 88 if not isinstance(other, _BaseVersion): 89 return NotImplemented 90 91 return self._key >= other._key 92 93 def __gt__(self, other: "_BaseVersion") -> bool: 94 if not isinstance(other, _BaseVersion): 95 return NotImplemented 96 97 return self._key > other._key 98 99 def __ne__(self, other: object) -> bool: 100 if not isinstance(other, _BaseVersion): 101 return NotImplemented 102 103 return self._key != other._key 104 105 106class LegacyVersion(_BaseVersion): 107 def __init__(self, version: str) -> None: 108 self._version = str(version) 109 self._key = _legacy_cmpkey(self._version) 110 111 warnings.warn( 112 "Creating a LegacyVersion has been deprecated and will be " 113 "removed in the next major release", 114 DeprecationWarning, 115 ) 116 117 def __str__(self) -> str: 118 return self._version 119 120 def __repr__(self) -> str: 121 return f"<LegacyVersion('{self}')>" 122 123 @property 124 def public(self) -> str: 125 return self._version 126 127 @property 128 def base_version(self) -> str: 129 return self._version 130 131 @property 132 def epoch(self) -> int: 133 return -1 134 135 @property 136 def release(self) -> None: 137 return None 138 139 @property 140 def pre(self) -> None: 141 return None 142 143 @property 144 def post(self) -> None: 145 return None 146 147 @property 148 def dev(self) -> None: 149 return None 150 151 @property 152 def local(self) -> None: 153 return None 154 155 @property 156 def is_prerelease(self) -> bool: 157 return False 158 159 @property 160 def is_postrelease(self) -> bool: 161 return False 162 163 @property 164 def is_devrelease(self) -> bool: 165 return False 166 167 168_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) 169 170_legacy_version_replacement_map = { 171 "pre": "c", 172 "preview": "c", 173 "-": "final-", 174 "rc": "c", 175 "dev": "@", 176} 177 178 179def _parse_version_parts(s: str) -> Iterator[str]: 180 for part in _legacy_version_component_re.split(s): 181 part = _legacy_version_replacement_map.get(part, part) 182 183 if not part or part == ".": 184 continue 185 186 if part[:1] in "0123456789": 187 # pad for numeric comparison 188 yield part.zfill(8) 189 else: 190 yield "*" + part 191 192 # ensure that alpha/beta/candidate are before final 193 yield "*final" 194 195 196def _legacy_cmpkey(version: str) -> LegacyCmpKey: 197 198 # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch 199 # greater than or equal to 0. This will effectively put the LegacyVersion, 200 # which uses the defacto standard originally implemented by setuptools, 201 # as before all PEP 440 versions. 202 epoch = -1 203 204 # This scheme is taken from pkg_resources.parse_version setuptools prior to 205 # it's adoption of the packaging library. 206 parts: List[str] = [] 207 for part in _parse_version_parts(version.lower()): 208 if part.startswith("*"): 209 # remove "-" before a prerelease tag 210 if part < "*final": 211 while parts and parts[-1] == "*final-": 212 parts.pop() 213 214 # remove trailing zeros from each series of numeric parts 215 while parts and parts[-1] == "00000000": 216 parts.pop() 217 218 parts.append(part) 219 220 return epoch, tuple(parts) 221 222 223# Deliberately not anchored to the start and end of the string, to make it 224# easier for 3rd party code to reuse 225VERSION_PATTERN = r""" 226 v? 227 (?: 228 (?:(?P<epoch>[0-9]+)!)? # epoch 229 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment 230 (?P<pre> # pre-release 231 [-_\.]? 232 (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview)) 233 [-_\.]? 234 (?P<pre_n>[0-9]+)? 235 )? 236 (?P<post> # post release 237 (?:-(?P<post_n1>[0-9]+)) 238 | 239 (?: 240 [-_\.]? 241 (?P<post_l>post|rev|r) 242 [-_\.]? 243 (?P<post_n2>[0-9]+)? 244 ) 245 )? 246 (?P<dev> # dev release 247 [-_\.]? 248 (?P<dev_l>dev) 249 [-_\.]? 250 (?P<dev_n>[0-9]+)? 251 )? 252 ) 253 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version 254""" 255 256 257class Version(_BaseVersion): 258 259 _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) 260 261 def __init__(self, version: str) -> None: 262 263 # Validate the version and parse it into pieces 264 match = self._regex.search(version) 265 if not match: 266 raise InvalidVersion(f"Invalid version: '{version}'") 267 268 # Store the parsed out pieces of the version 269 self._version = _Version( 270 epoch=int(match.group("epoch")) if match.group("epoch") else 0, 271 release=tuple(int(i) for i in match.group("release").split(".")), 272 pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), 273 post=_parse_letter_version( 274 match.group("post_l"), match.group("post_n1") or match.group("post_n2") 275 ), 276 dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), 277 local=_parse_local_version(match.group("local")), 278 ) 279 280 # Generate a key which will be used for sorting 281 self._key = _cmpkey( 282 self._version.epoch, 283 self._version.release, 284 self._version.pre, 285 self._version.post, 286 self._version.dev, 287 self._version.local, 288 ) 289 290 def __repr__(self) -> str: 291 return f"<Version('{self}')>" 292 293 def __str__(self) -> str: 294 parts = [] 295 296 # Epoch 297 if self.epoch != 0: 298 parts.append(f"{self.epoch}!") 299 300 # Release segment 301 parts.append(".".join(str(x) for x in self.release)) 302 303 # Pre-release 304 if self.pre is not None: 305 parts.append("".join(str(x) for x in self.pre)) 306 307 # Post-release 308 if self.post is not None: 309 parts.append(f".post{self.post}") 310 311 # Development release 312 if self.dev is not None: 313 parts.append(f".dev{self.dev}") 314 315 # Local version segment 316 if self.local is not None: 317 parts.append(f"+{self.local}") 318 319 return "".join(parts) 320 321 @property 322 def epoch(self) -> int: 323 _epoch: int = self._version.epoch 324 return _epoch 325 326 @property 327 def release(self) -> Tuple[int, ...]: 328 _release: Tuple[int, ...] = self._version.release 329 return _release 330 331 @property 332 def pre(self) -> Optional[Tuple[str, int]]: 333 _pre: Optional[Tuple[str, int]] = self._version.pre 334 return _pre 335 336 @property 337 def post(self) -> Optional[int]: 338 return self._version.post[1] if self._version.post else None 339 340 @property 341 def dev(self) -> Optional[int]: 342 return self._version.dev[1] if self._version.dev else None 343 344 @property 345 def local(self) -> Optional[str]: 346 if self._version.local: 347 return ".".join(str(x) for x in self._version.local) 348 else: 349 return None 350 351 @property 352 def public(self) -> str: 353 return str(self).split("+", 1)[0] 354 355 @property 356 def base_version(self) -> str: 357 parts = [] 358 359 # Epoch 360 if self.epoch != 0: 361 parts.append(f"{self.epoch}!") 362 363 # Release segment 364 parts.append(".".join(str(x) for x in self.release)) 365 366 return "".join(parts) 367 368 @property 369 def is_prerelease(self) -> bool: 370 return self.dev is not None or self.pre is not None 371 372 @property 373 def is_postrelease(self) -> bool: 374 return self.post is not None 375 376 @property 377 def is_devrelease(self) -> bool: 378 return self.dev is not None 379 380 @property 381 def major(self) -> int: 382 return self.release[0] if len(self.release) >= 1 else 0 383 384 @property 385 def minor(self) -> int: 386 return self.release[1] if len(self.release) >= 2 else 0 387 388 @property 389 def micro(self) -> int: 390 return self.release[2] if len(self.release) >= 3 else 0 391 392 393def _parse_letter_version( 394 letter: str, number: Union[str, bytes, SupportsInt] 395) -> Optional[Tuple[str, int]]: 396 397 if letter: 398 # We consider there to be an implicit 0 in a pre-release if there is 399 # not a numeral associated with it. 400 if number is None: 401 number = 0 402 403 # We normalize any letters to their lower case form 404 letter = letter.lower() 405 406 # We consider some words to be alternate spellings of other words and 407 # in those cases we want to normalize the spellings to our preferred 408 # spelling. 409 if letter == "alpha": 410 letter = "a" 411 elif letter == "beta": 412 letter = "b" 413 elif letter in ["c", "pre", "preview"]: 414 letter = "rc" 415 elif letter in ["rev", "r"]: 416 letter = "post" 417 418 return letter, int(number) 419 if not letter and number: 420 # We assume if we are given a number, but we are not given a letter 421 # then this is using the implicit post release syntax (e.g. 1.0-1) 422 letter = "post" 423 424 return letter, int(number) 425 426 return None 427 428 429_local_version_separators = re.compile(r"[\._-]") 430 431 432def _parse_local_version(local: str) -> Optional[LocalType]: 433 """ 434 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). 435 """ 436 if local is not None: 437 return tuple( 438 part.lower() if not part.isdigit() else int(part) 439 for part in _local_version_separators.split(local) 440 ) 441 return None 442 443 444def _cmpkey( 445 epoch: int, 446 release: Tuple[int, ...], 447 pre: Optional[Tuple[str, int]], 448 post: Optional[Tuple[str, int]], 449 dev: Optional[Tuple[str, int]], 450 local: Optional[Tuple[SubLocalType]], 451) -> CmpKey: 452 453 # When we compare a release version, we want to compare it with all of the 454 # trailing zeros removed. So we'll use a reverse the list, drop all the now 455 # leading zeros until we come to something non zero, then take the rest 456 # re-reverse it back into the correct order and make it a tuple and use 457 # that for our sorting key. 458 _release = tuple( 459 reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) 460 ) 461 462 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. 463 # We'll do this by abusing the pre segment, but we _only_ want to do this 464 # if there is not a pre or a post segment. If we have one of those then 465 # the normal sorting rules will handle this case correctly. 466 if pre is None and post is None and dev is not None: 467 _pre: PrePostDevType = NegativeInfinity 468 # Versions without a pre-release (except as noted above) should sort after 469 # those with one. 470 elif pre is None: 471 _pre = Infinity 472 else: 473 _pre = pre 474 475 # Versions without a post segment should sort before those with one. 476 if post is None: 477 _post: PrePostDevType = NegativeInfinity 478 479 else: 480 _post = post 481 482 # Versions without a development segment should sort after those with one. 483 if dev is None: 484 _dev: PrePostDevType = Infinity 485 486 else: 487 _dev = dev 488 489 if local is None: 490 # Versions without a local segment should sort before those with one. 491 _local: LocalType = NegativeInfinity 492 else: 493 # Versions with a local segment need that segment parsed to implement 494 # the sorting rules in PEP440. 495 # - Alpha numeric segments sort before numeric segments 496 # - Alpha numeric segments sort lexicographically 497 # - Numeric segments sort numerically 498 # - Shorter versions sort before longer versions when the prefixes 499 # match exactly 500 _local = tuple( 501 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local 502 ) 503 504 return epoch, _release, _pre, _post, _dev, _local 505