Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/version.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

262 statements  

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 

10from __future__ import annotations 

11 

12import re 

13import sys 

14import typing 

15from typing import Any, Callable, Literal, SupportsInt, Tuple, TypedDict, Union 

16 

17from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType 

18 

19if typing.TYPE_CHECKING: 

20 from typing_extensions import Self, Unpack 

21 

22_LETTER_NORMALIZATION = { 

23 "alpha": "a", 

24 "beta": "b", 

25 "c": "rc", 

26 "pre": "rc", 

27 "preview": "rc", 

28 "rev": "post", 

29 "r": "post", 

30} 

31 

32__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"] 

33 

34LocalType = Tuple[Union[int, str], ...] 

35 

36CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] 

37CmpLocalType = Union[ 

38 NegativeInfinityType, 

39 Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], 

40] 

41CmpKey = Tuple[ 

42 int, 

43 Tuple[int, ...], 

44 CmpPrePostDevType, 

45 CmpPrePostDevType, 

46 CmpPrePostDevType, 

47 CmpLocalType, 

48] 

49VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] 

50 

51 

52class _VersionReplace(TypedDict, total=False): 

53 epoch: int | None 

54 release: tuple[int, ...] | None 

55 pre: tuple[Literal["a", "b", "rc"], int] | None 

56 post: int | None 

57 dev: int | None 

58 local: str | None 

59 

60 

61def parse(version: str) -> Version: 

62 """Parse the given version string. 

63 

64 >>> parse('1.0.dev1') 

65 <Version('1.0.dev1')> 

66 

67 :param version: The version string to parse. 

68 :raises InvalidVersion: When the version string is not a valid version. 

69 """ 

70 return Version(version) 

71 

72 

73class InvalidVersion(ValueError): 

74 """Raised when a version string is not a valid version. 

75 

76 >>> Version("invalid") 

77 Traceback (most recent call last): 

78 ... 

79 packaging.version.InvalidVersion: Invalid version: 'invalid' 

80 """ 

81 

82 

83class _BaseVersion: 

84 __slots__ = () 

85 

86 @property 

87 def _key(self) -> tuple[Any, ...]: 

88 raise NotImplementedError # pragma: no cover 

89 

90 def __hash__(self) -> int: 

91 return hash(self._key) 

92 

93 # Please keep the duplicated `isinstance` check 

94 # in the six comparisons hereunder 

95 # unless you find a way to avoid adding overhead function calls. 

96 def __lt__(self, other: _BaseVersion) -> bool: 

97 if not isinstance(other, _BaseVersion): 

98 return NotImplemented 

99 

100 return self._key < other._key 

101 

102 def __le__(self, other: _BaseVersion) -> bool: 

103 if not isinstance(other, _BaseVersion): 

104 return NotImplemented 

105 

106 return self._key <= other._key 

107 

108 def __eq__(self, other: object) -> bool: 

109 if not isinstance(other, _BaseVersion): 

110 return NotImplemented 

111 

112 return self._key == other._key 

113 

114 def __ge__(self, other: _BaseVersion) -> bool: 

115 if not isinstance(other, _BaseVersion): 

116 return NotImplemented 

117 

118 return self._key >= other._key 

119 

120 def __gt__(self, other: _BaseVersion) -> bool: 

121 if not isinstance(other, _BaseVersion): 

122 return NotImplemented 

123 

124 return self._key > other._key 

125 

126 def __ne__(self, other: object) -> bool: 

127 if not isinstance(other, _BaseVersion): 

128 return NotImplemented 

129 

130 return self._key != other._key 

131 

132 

133# Deliberately not anchored to the start and end of the string, to make it 

134# easier for 3rd party code to reuse 

135 

136# Note that ++ doesn't behave identically on CPython and PyPy, so not using it here 

137_VERSION_PATTERN = r""" 

138 v?+ # optional leading v 

139 (?: 

140 (?:(?P<epoch>[0-9]+)!)?+ # epoch 

141 (?P<release>[0-9]+(?:\.[0-9]+)*+) # release segment 

142 (?P<pre> # pre-release 

143 [._-]?+ 

144 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc) 

145 [._-]?+ 

146 (?P<pre_n>[0-9]+)? 

147 )?+ 

148 (?P<post> # post release 

149 (?:-(?P<post_n1>[0-9]+)) 

150 | 

151 (?: 

152 [._-]? 

153 (?P<post_l>post|rev|r) 

154 [._-]? 

155 (?P<post_n2>[0-9]+)? 

156 ) 

157 )?+ 

158 (?P<dev> # dev release 

159 [._-]?+ 

160 (?P<dev_l>dev) 

161 [._-]?+ 

162 (?P<dev_n>[0-9]+)? 

163 )?+ 

164 ) 

165 (?:\+ 

166 (?P<local> # local version 

167 [a-z0-9]+ 

168 (?:[._-][a-z0-9]+)*+ 

169 ) 

170 )?+ 

171""" 

172 

173_VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?") 

174 

175VERSION_PATTERN = ( 

176 _VERSION_PATTERN_OLD if sys.version_info < (3, 11) else _VERSION_PATTERN 

177) 

178""" 

179A string containing the regular expression used to match a valid version. 

180 

181The pattern is not anchored at either end, and is intended for embedding in larger 

182expressions (for example, matching a version number as part of a file name). The 

183regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE`` 

184flags set. 

185 

186:meta hide-value: 

187""" 

188 

189 

190# Validation pattern for local version in replace() 

191_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE) 

192 

193 

194def _validate_epoch(value: object, /) -> int: 

195 epoch = value or 0 

196 if isinstance(epoch, int) and epoch >= 0: 

197 return epoch 

198 msg = f"epoch must be non-negative integer, got {epoch}" 

199 raise InvalidVersion(msg) 

200 

201 

202def _validate_release(value: object, /) -> tuple[int, ...]: 

203 release = (0,) if value is None else value 

204 if ( 

205 isinstance(release, tuple) 

206 and len(release) > 0 

207 and all(isinstance(i, int) and i >= 0 for i in release) 

208 ): 

209 return release 

210 msg = f"release must be a non-empty tuple of non-negative integers, got {release}" 

211 raise InvalidVersion(msg) 

212 

213 

214def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None: 

215 if value is None: 

216 return value 

217 if ( 

218 isinstance(value, tuple) 

219 and len(value) == 2 

220 and value[0] in ("a", "b", "rc") 

221 and isinstance(value[1], int) 

222 and value[1] >= 0 

223 ): 

224 return value 

225 msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}" 

226 raise InvalidVersion(msg) 

227 

228 

229def _validate_post(value: object, /) -> tuple[Literal["post"], int] | None: 

230 if value is None: 

231 return value 

232 if isinstance(value, int) and value >= 0: 

233 return ("post", value) 

234 msg = f"post must be non-negative integer, got {value}" 

235 raise InvalidVersion(msg) 

236 

237 

238def _validate_dev(value: object, /) -> tuple[Literal["dev"], int] | None: 

239 if value is None: 

240 return value 

241 if isinstance(value, int) and value >= 0: 

242 return ("dev", value) 

243 msg = f"dev must be non-negative integer, got {value}" 

244 raise InvalidVersion(msg) 

245 

246 

247def _validate_local(value: object, /) -> LocalType | None: 

248 if value is None: 

249 return value 

250 if isinstance(value, str) and _LOCAL_PATTERN.fullmatch(value): 

251 return _parse_local_version(value) 

252 msg = f"local must be a valid version string, got {value!r}" 

253 raise InvalidVersion(msg) 

254 

255 

256class Version(_BaseVersion): 

257 """This class abstracts handling of a project's versions. 

258 

259 A :class:`Version` instance is comparison aware and can be compared and 

260 sorted using the standard Python interfaces. 

261 

262 >>> v1 = Version("1.0a5") 

263 >>> v2 = Version("1.0") 

264 >>> v1 

265 <Version('1.0a5')> 

266 >>> v2 

267 <Version('1.0')> 

268 >>> v1 < v2 

269 True 

270 >>> v1 == v2 

271 False 

272 >>> v1 > v2 

273 False 

274 >>> v1 >= v2 

275 False 

276 >>> v1 <= v2 

277 True 

278 """ 

279 

280 __slots__ = ("_dev", "_epoch", "_key_cache", "_local", "_post", "_pre", "_release") 

281 __match_args__ = ("_str",) 

282 

283 _regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE) 

284 

285 _epoch: int 

286 _release: tuple[int, ...] 

287 _dev: tuple[str, int] | None 

288 _pre: tuple[str, int] | None 

289 _post: tuple[str, int] | None 

290 _local: LocalType | None 

291 

292 _key_cache: CmpKey | None 

293 

294 def __init__(self, version: str) -> None: 

295 """Initialize a Version object. 

296 

297 :param version: 

298 The string representation of a version which will be parsed and normalized 

299 before use. 

300 :raises InvalidVersion: 

301 If the ``version`` does not conform to PEP 440 in any way then this 

302 exception will be raised. 

303 """ 

304 # Validate the version and parse it into pieces 

305 match = self._regex.fullmatch(version) 

306 if not match: 

307 raise InvalidVersion(f"Invalid version: {version!r}") 

308 self._epoch = int(match.group("epoch")) if match.group("epoch") else 0 

309 self._release = tuple(map(int, match.group("release").split("."))) 

310 self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n")) 

311 self._post = _parse_letter_version( 

312 match.group("post_l"), match.group("post_n1") or match.group("post_n2") 

313 ) 

314 self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n")) 

315 self._local = _parse_local_version(match.group("local")) 

316 

317 # Key which will be used for sorting 

318 self._key_cache = None 

319 

320 def __replace__(self, **kwargs: Unpack[_VersionReplace]) -> Self: 

321 epoch = _validate_epoch(kwargs["epoch"]) if "epoch" in kwargs else self._epoch 

322 release = ( 

323 _validate_release(kwargs["release"]) 

324 if "release" in kwargs 

325 else self._release 

326 ) 

327 pre = _validate_pre(kwargs["pre"]) if "pre" in kwargs else self._pre 

328 post = _validate_post(kwargs["post"]) if "post" in kwargs else self._post 

329 dev = _validate_dev(kwargs["dev"]) if "dev" in kwargs else self._dev 

330 local = _validate_local(kwargs["local"]) if "local" in kwargs else self._local 

331 

332 if ( 

333 epoch == self._epoch 

334 and release == self._release 

335 and pre == self._pre 

336 and post == self._post 

337 and dev == self._dev 

338 and local == self._local 

339 ): 

340 return self 

341 

342 new_version = self.__class__.__new__(self.__class__) 

343 new_version._key_cache = None 

344 new_version._epoch = epoch 

345 new_version._release = release 

346 new_version._pre = pre 

347 new_version._post = post 

348 new_version._dev = dev 

349 new_version._local = local 

350 

351 return new_version 

352 

353 @property 

354 def _key(self) -> CmpKey: 

355 if self._key_cache is None: 

356 self._key_cache = _cmpkey( 

357 self._epoch, 

358 self._release, 

359 self._pre, 

360 self._post, 

361 self._dev, 

362 self._local, 

363 ) 

364 return self._key_cache 

365 

366 def __repr__(self) -> str: 

367 """A representation of the Version that shows all internal state. 

368 

369 >>> Version('1.0.0') 

370 <Version('1.0.0')> 

371 """ 

372 return f"<Version('{self}')>" 

373 

374 def __str__(self) -> str: 

375 """A string representation of the version that can be round-tripped. 

376 

377 >>> str(Version("1.0a5")) 

378 '1.0a5' 

379 """ 

380 parts = [self.base_version] 

381 

382 # Pre-release 

383 if self.pre is not None: 

384 parts.append("".join(map(str, self.pre))) 

385 

386 # Post-release 

387 if self.post is not None: 

388 parts.append(f".post{self.post}") 

389 

390 # Development release 

391 if self.dev is not None: 

392 parts.append(f".dev{self.dev}") 

393 

394 # Local version segment 

395 if self.local is not None: 

396 parts.append(f"+{self.local}") 

397 

398 return "".join(parts) 

399 

400 @property 

401 def _str(self) -> str: 

402 """Internal property for match_args""" 

403 return str(self) 

404 

405 @property 

406 def epoch(self) -> int: 

407 """The epoch of the version. 

408 

409 >>> Version("2.0.0").epoch 

410 0 

411 >>> Version("1!2.0.0").epoch 

412 1 

413 """ 

414 return self._epoch 

415 

416 @property 

417 def release(self) -> tuple[int, ...]: 

418 """The components of the "release" segment of the version. 

419 

420 >>> Version("1.2.3").release 

421 (1, 2, 3) 

422 >>> Version("2.0.0").release 

423 (2, 0, 0) 

424 >>> Version("1!2.0.0.post0").release 

425 (2, 0, 0) 

426 

427 Includes trailing zeroes but not the epoch or any pre-release / development / 

428 post-release suffixes. 

429 """ 

430 return self._release 

431 

432 @property 

433 def pre(self) -> tuple[str, int] | None: 

434 """The pre-release segment of the version. 

435 

436 >>> print(Version("1.2.3").pre) 

437 None 

438 >>> Version("1.2.3a1").pre 

439 ('a', 1) 

440 >>> Version("1.2.3b1").pre 

441 ('b', 1) 

442 >>> Version("1.2.3rc1").pre 

443 ('rc', 1) 

444 """ 

445 return self._pre 

446 

447 @property 

448 def post(self) -> int | None: 

449 """The post-release number of the version. 

450 

451 >>> print(Version("1.2.3").post) 

452 None 

453 >>> Version("1.2.3.post1").post 

454 1 

455 """ 

456 return self._post[1] if self._post else None 

457 

458 @property 

459 def dev(self) -> int | None: 

460 """The development number of the version. 

461 

462 >>> print(Version("1.2.3").dev) 

463 None 

464 >>> Version("1.2.3.dev1").dev 

465 1 

466 """ 

467 return self._dev[1] if self._dev else None 

468 

469 @property 

470 def local(self) -> str | None: 

471 """The local version segment of the version. 

472 

473 >>> print(Version("1.2.3").local) 

474 None 

475 >>> Version("1.2.3+abc").local 

476 'abc' 

477 """ 

478 if self._local: 

479 return ".".join(str(x) for x in self._local) 

480 else: 

481 return None 

482 

483 @property 

484 def public(self) -> str: 

485 """The public portion of the version. 

486 

487 >>> Version("1.2.3").public 

488 '1.2.3' 

489 >>> Version("1.2.3+abc").public 

490 '1.2.3' 

491 >>> Version("1!1.2.3dev1+abc").public 

492 '1!1.2.3.dev1' 

493 """ 

494 return str(self).split("+", 1)[0] 

495 

496 @property 

497 def base_version(self) -> str: 

498 """The "base version" of the version. 

499 

500 >>> Version("1.2.3").base_version 

501 '1.2.3' 

502 >>> Version("1.2.3+abc").base_version 

503 '1.2.3' 

504 >>> Version("1!1.2.3dev1+abc").base_version 

505 '1!1.2.3' 

506 

507 The "base version" is the public version of the project without any pre or post 

508 release markers. 

509 """ 

510 release_segment = ".".join(map(str, self.release)) 

511 return f"{self.epoch}!{release_segment}" if self.epoch else release_segment 

512 

513 @property 

514 def is_prerelease(self) -> bool: 

515 """Whether this version is a pre-release. 

516 

517 >>> Version("1.2.3").is_prerelease 

518 False 

519 >>> Version("1.2.3a1").is_prerelease 

520 True 

521 >>> Version("1.2.3b1").is_prerelease 

522 True 

523 >>> Version("1.2.3rc1").is_prerelease 

524 True 

525 >>> Version("1.2.3dev1").is_prerelease 

526 True 

527 """ 

528 return self.dev is not None or self.pre is not None 

529 

530 @property 

531 def is_postrelease(self) -> bool: 

532 """Whether this version is a post-release. 

533 

534 >>> Version("1.2.3").is_postrelease 

535 False 

536 >>> Version("1.2.3.post1").is_postrelease 

537 True 

538 """ 

539 return self.post is not None 

540 

541 @property 

542 def is_devrelease(self) -> bool: 

543 """Whether this version is a development release. 

544 

545 >>> Version("1.2.3").is_devrelease 

546 False 

547 >>> Version("1.2.3.dev1").is_devrelease 

548 True 

549 """ 

550 return self.dev is not None 

551 

552 @property 

553 def major(self) -> int: 

554 """The first item of :attr:`release` or ``0`` if unavailable. 

555 

556 >>> Version("1.2.3").major 

557 1 

558 """ 

559 return self.release[0] if len(self.release) >= 1 else 0 

560 

561 @property 

562 def minor(self) -> int: 

563 """The second item of :attr:`release` or ``0`` if unavailable. 

564 

565 >>> Version("1.2.3").minor 

566 2 

567 >>> Version("1").minor 

568 0 

569 """ 

570 return self.release[1] if len(self.release) >= 2 else 0 

571 

572 @property 

573 def micro(self) -> int: 

574 """The third item of :attr:`release` or ``0`` if unavailable. 

575 

576 >>> Version("1.2.3").micro 

577 3 

578 >>> Version("1").micro 

579 0 

580 """ 

581 return self.release[2] if len(self.release) >= 3 else 0 

582 

583 

584class _TrimmedRelease(Version): 

585 __slots__ = () 

586 

587 def __init__(self, version: str | Version) -> None: 

588 if isinstance(version, Version): 

589 self._epoch = version._epoch 

590 self._release = version._release 

591 self._dev = version._dev 

592 self._pre = version._pre 

593 self._post = version._post 

594 self._local = version._local 

595 self._key_cache = version._key_cache 

596 return 

597 super().__init__(version) # pragma: no cover 

598 

599 @property 

600 def release(self) -> tuple[int, ...]: 

601 """ 

602 Release segment without any trailing zeros. 

603 

604 >>> _TrimmedRelease('1.0.0').release 

605 (1,) 

606 >>> _TrimmedRelease('0.0').release 

607 (0,) 

608 """ 

609 # Unlike _strip_trailing_zeros, this leaves one 0. 

610 rel = super().release 

611 i = len(rel) 

612 while i > 1 and rel[i - 1] == 0: 

613 i -= 1 

614 return rel[:i] 

615 

616 

617def _parse_letter_version( 

618 letter: str | None, number: str | bytes | SupportsInt | None 

619) -> tuple[str, int] | None: 

620 if letter: 

621 # We normalize any letters to their lower case form 

622 letter = letter.lower() 

623 

624 # We consider some words to be alternate spellings of other words and 

625 # in those cases we want to normalize the spellings to our preferred 

626 # spelling. 

627 letter = _LETTER_NORMALIZATION.get(letter, letter) 

628 

629 # We consider there to be an implicit 0 in a pre-release if there is 

630 # not a numeral associated with it. 

631 return letter, int(number or 0) 

632 

633 if number: 

634 # We assume if we are given a number, but we are not given a letter 

635 # then this is using the implicit post release syntax (e.g. 1.0-1) 

636 return "post", int(number) 

637 

638 return None 

639 

640 

641_local_version_separators = re.compile(r"[\._-]") 

642 

643 

644def _parse_local_version(local: str | None) -> LocalType | None: 

645 """ 

646 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). 

647 """ 

648 if local is not None: 

649 return tuple( 

650 part.lower() if not part.isdigit() else int(part) 

651 for part in _local_version_separators.split(local) 

652 ) 

653 return None 

654 

655 

656def _strip_trailing_zeros(release: tuple[int, ...]) -> tuple[int, ...]: 

657 # We want to strip trailing zeros from a tuple of values. This starts 

658 # from the end and returns as soon as it finds a non-zero value. When 

659 # reading a lot of versions, this is a fairly hot function, so not using 

660 # enumerate/reversed, which is slightly slower. 

661 for i in range(len(release) - 1, -1, -1): 

662 if release[i] != 0: 

663 return release[: i + 1] 

664 return () 

665 

666 

667def _cmpkey( 

668 epoch: int, 

669 release: tuple[int, ...], 

670 pre: tuple[str, int] | None, 

671 post: tuple[str, int] | None, 

672 dev: tuple[str, int] | None, 

673 local: LocalType | None, 

674) -> CmpKey: 

675 # When we compare a release version, we want to compare it with all of the 

676 # trailing zeros removed. We will use this for our sorting key. 

677 _release = _strip_trailing_zeros(release) 

678 

679 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. 

680 # We'll do this by abusing the pre segment, but we _only_ want to do this 

681 # if there is not a pre or a post segment. If we have one of those then 

682 # the normal sorting rules will handle this case correctly. 

683 if pre is None and post is None and dev is not None: 

684 _pre: CmpPrePostDevType = NegativeInfinity 

685 # Versions without a pre-release (except as noted above) should sort after 

686 # those with one. 

687 elif pre is None: 

688 _pre = Infinity 

689 else: 

690 _pre = pre 

691 

692 # Versions without a post segment should sort before those with one. 

693 if post is None: 

694 _post: CmpPrePostDevType = NegativeInfinity 

695 

696 else: 

697 _post = post 

698 

699 # Versions without a development segment should sort after those with one. 

700 if dev is None: 

701 _dev: CmpPrePostDevType = Infinity 

702 

703 else: 

704 _dev = dev 

705 

706 if local is None: 

707 # Versions without a local segment should sort before those with one. 

708 _local: CmpLocalType = NegativeInfinity 

709 else: 

710 # Versions with a local segment need that segment parsed to implement 

711 # the sorting rules in PEP440. 

712 # - Alpha numeric segments sort before numeric segments 

713 # - Alpha numeric segments sort lexicographically 

714 # - Numeric segments sort numerically 

715 # - Shorter versions sort before longer versions when the prefixes 

716 # match exactly 

717 _local = tuple( 

718 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local 

719 ) 

720 

721 return epoch, _release, _pre, _post, _dev, _local