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

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

443 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, normalize_pre, Version, _cmpkey 

8""" 

9 

10from __future__ import annotations 

11 

12__lazy_modules__ = {"functools"} 

13 

14import builtins 

15import re 

16import sys 

17import typing 

18from collections.abc import Callable 

19from typing import ( 

20 Any, 

21 Literal, 

22 NamedTuple, 

23 SupportsInt, 

24) 

25 

26if typing.TYPE_CHECKING: 

27 from typing_extensions import Self 

28 

29if sys.version_info >= (3, 13): # pragma: no cover 

30 from warnings import deprecated as _deprecated 

31elif typing.TYPE_CHECKING: 

32 from typing_extensions import deprecated as _deprecated 

33else: # pragma: no cover 

34 import functools 

35 import warnings 

36 

37 def _deprecated(message: str) -> object: 

38 def decorator(func: Callable[[...], object]) -> object: 

39 @functools.wraps(func) 

40 def wrapper(*args: object, **kwargs: object) -> object: 

41 warnings.warn( 

42 message, 

43 category=DeprecationWarning, 

44 stacklevel=2, 

45 ) 

46 return func(*args, **kwargs) 

47 

48 return wrapper 

49 

50 return decorator 

51 

52 

53_LETTER_NORMALIZATION = { 

54 "alpha": "a", 

55 "beta": "b", 

56 "c": "rc", 

57 "pre": "rc", 

58 "preview": "rc", 

59 "rev": "post", 

60 "r": "post", 

61} 

62 

63__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "normalize_pre", "parse"] 

64 

65 

66def __dir__() -> list[str]: 

67 return __all__ 

68 

69 

70LocalType = tuple[int | str, ...] 

71 

72CmpLocalType = tuple[tuple[int, str], ...] 

73CmpSuffix = tuple[int, int, int, int, int, int] 

74CmpKey = ( 

75 tuple[int, tuple[int, ...], CmpSuffix] 

76 | tuple[int, tuple[int, ...], CmpSuffix, CmpLocalType] 

77) 

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

79 

80 

81# Sentinel for __replace__, typed as Any so it can be the default for any field. 

82# mypy is missing sentinel currently 

83_UNSET: Any = object() if sys.version_info < (3, 15) else builtins.sentinel("_UNSET") # type: ignore[attr-defined] 

84 

85 

86def normalize_pre(letter: str, /) -> str: 

87 """Normalize the pre-release segment of a version string. 

88 

89 Returns a lowercase version of the string if not a known pre-release 

90 identifier. 

91 

92 >>> normalize_pre('alpha') 

93 'a' 

94 >>> normalize_pre('BETA') 

95 'b' 

96 >>> normalize_pre('rc') 

97 'rc' 

98 

99 :param letter: 

100 

101 .. versionadded:: 26.1 

102 """ 

103 letter = letter.lower() 

104 return _LETTER_NORMALIZATION.get(letter, letter) 

105 

106 

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

108 """Parse the given version string. 

109 

110 This is identical to the :class:`Version` constructor. 

111 

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

113 <Version('1.0.dev1')> 

114 

115 :param version: The version string to parse. 

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

117 """ 

118 return Version(version) 

119 

120 

121class InvalidVersion(ValueError): 

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

123 

124 >>> Version("invalid") 

125 Traceback (most recent call last): 

126 ... 

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

128 """ 

129 

130 

131class _BaseVersion: 

132 __slots__ = () 

133 

134 # This can also be a normal member (see the packaging_legacy package); 

135 # we are just requiring it to be readable. Actually defining a property 

136 # has runtime effect on subclasses, so it's typing only. 

137 if typing.TYPE_CHECKING: 

138 

139 @property 

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

141 

142 def __hash__(self) -> int: 

143 return hash(self._key) 

144 

145 # Please keep the duplicated `isinstance` check 

146 # in the six comparisons hereunder 

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

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

149 if not isinstance(other, _BaseVersion): 

150 return NotImplemented 

151 

152 return self._key < other._key 

153 

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

155 if not isinstance(other, _BaseVersion): 

156 return NotImplemented 

157 

158 return self._key <= other._key 

159 

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

161 if not isinstance(other, _BaseVersion): 

162 return NotImplemented 

163 

164 return self._key == other._key 

165 

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

167 if not isinstance(other, _BaseVersion): 

168 return NotImplemented 

169 

170 return self._key >= other._key 

171 

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

173 if not isinstance(other, _BaseVersion): 

174 return NotImplemented 

175 

176 return self._key > other._key 

177 

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

179 if not isinstance(other, _BaseVersion): 

180 return NotImplemented 

181 

182 return self._key != other._key 

183 

184 

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

186# easier for 3rd party code to reuse 

187 

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

189_VERSION_PATTERN = r""" 

190 v?+ # optional leading v 

191 (?a: 

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

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

194 (?P<pre> # pre-release 

195 [._-]?+ 

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

197 [._-]?+ 

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

199 )?+ 

200 (?P<post> # post release 

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

202 | 

203 (?: 

204 [._-]? 

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

206 [._-]? 

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

208 ) 

209 )?+ 

210 (?P<dev> # dev release 

211 [._-]?+ 

212 (?P<dev_l>dev) 

213 [._-]?+ 

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

215 )?+ 

216 ) 

217 (?a:\+ 

218 (?P<local> # local version 

219 [a-z0-9]+ 

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

221 ) 

222 )?+ 

223""" 

224 

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

226 

227# Possessive qualifiers were added in Python 3.11. 

228# CPython 3.11.0-3.11.4 had a bug: https://github.com/python/cpython/pull/107795 

229# Older PyPy also had a bug. 

230VERSION_PATTERN = ( 

231 _VERSION_PATTERN_OLD 

232 if (sys.implementation.name == "cpython" and sys.version_info < (3, 11, 5)) 

233 or (sys.implementation.name == "pypy" and sys.version_info < (3, 11, 13)) 

234 or sys.version_info < (3, 11) 

235 else _VERSION_PATTERN 

236) 

237""" 

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

239 

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

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

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

243flags set. 

244 

245.. versionchanged:: 26.0 

246 

247 The regex now uses possessive qualifiers on Python 3.11 if they are 

248 supported (CPython 3.11.5+, PyPy 3.11.13+). 

249 

250:meta hide-value: 

251""" 

252 

253 

254# Validation pattern for local version in replace() 

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

256 

257# Fast path: If a version has only digits and dots then we 

258# can skip the regex and parse it as a release segment 

259_SIMPLE_VERSION_INDICATORS = frozenset(".0123456789") 

260 

261 

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

263 epoch = value or 0 

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

265 return epoch 

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

267 raise InvalidVersion(msg) 

268 

269 

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

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

272 if ( 

273 isinstance(release, tuple) 

274 and len(release) > 0 

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

276 ): 

277 return release 

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

279 raise InvalidVersion(msg) 

280 

281 

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

283 if value is None: 

284 return value 

285 if isinstance(value, tuple) and len(value) == 2: 

286 letter, number = value 

287 # The letter must be a string before it can be normalized. 

288 if ( 

289 isinstance(letter, str) 

290 and (normalized := normalize_pre(letter)) in {"a", "b", "rc"} 

291 and isinstance(number, int) 

292 and number >= 0 

293 ): 

294 # type checkers can't infer the Literal type here on letter 

295 return (normalized, number) # type: ignore[return-value] 

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

297 raise InvalidVersion(msg) 

298 

299 

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

301 if value is None: 

302 return value 

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

304 return ("post", value) 

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

306 raise InvalidVersion(msg) 

307 

308 

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

310 if value is None: 

311 return value 

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

313 return ("dev", value) 

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

315 raise InvalidVersion(msg) 

316 

317 

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

319 if value is None: 

320 return value 

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

322 return _parse_local_version(value) 

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

324 raise InvalidVersion(msg) 

325 

326 

327# Backward compatibility for internals before 26.0. Do not use. 

328class _Version(NamedTuple): 

329 epoch: int 

330 release: tuple[int, ...] 

331 dev: tuple[Literal["dev"], int] | None 

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

333 post: tuple[Literal["post"], int] | None 

334 local: LocalType | None 

335 

336 

337class Version(_BaseVersion): 

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

339 

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

341 sorted using the standard Python interfaces. 

342 

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

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

345 >>> v1 

346 <Version('1.0a5')> 

347 >>> v2 

348 <Version('1.0')> 

349 >>> v1 < v2 

350 True 

351 >>> v1 == v2 

352 False 

353 >>> v1 > v2 

354 False 

355 >>> v1 >= v2 

356 False 

357 >>> v1 <= v2 

358 True 

359 

360 :class:`Version` is immutable; use :meth:`__replace__` to change 

361 part of a version. 

362 

363 Instances are safe to serialize with :mod:`pickle`. They use a stable 

364 format so the same pickle can be loaded in future packaging releases. 

365 

366 .. versionchanged:: 26.2 

367 

368 Added a stable pickle format. Pickles created with packaging 26.2+ can 

369 be unpickled with future releases. Backward compatibility with pickles 

370 from packaging < 26.2 is supported but may be removed in a future 

371 release. 

372 """ 

373 

374 __slots__ = ( 

375 "_dev", 

376 "_epoch", 

377 "_hash_cache", 

378 "_key_cache", 

379 "_local", 

380 "_post", 

381 "_pre", 

382 "_release", 

383 ) 

384 __match_args__ = ("_str",) 

385 """ 

386 Pattern matching is supported on Python 3.10+. 

387 

388 .. versionadded:: 26.0 

389 

390 :meta hide-value: 

391 """ 

392 

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

394 

395 _epoch: int 

396 _release: tuple[int, ...] 

397 _dev: tuple[Literal["dev"], int] | None 

398 _pre: tuple[Literal["a", "b", "rc"], int] | None 

399 _post: tuple[Literal["post"], int] | None 

400 _local: LocalType | None 

401 

402 _hash_cache: int | None 

403 _key_cache: CmpKey | None 

404 

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

406 """Initialize a Version object. 

407 

408 :param version: 

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

410 before use. 

411 :raises InvalidVersion: 

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

413 exception will be raised. 

414 """ 

415 try: 

416 is_simple = _SIMPLE_VERSION_INDICATORS.issuperset(version) 

417 except TypeError: 

418 raise InvalidVersion(f"Invalid version: {version!r}") from None 

419 

420 if is_simple: 

421 try: 

422 self._release = tuple(map(int, version.split("."))) 

423 except AttributeError: 

424 raise InvalidVersion(f"Invalid version: {version!r}") from None 

425 except ValueError: 

426 # Empty parts (from "1..2", ".1", etc.) are invalid versions. 

427 # Any other ValueError (e.g. int str-digits limit) should 

428 # propagate to the caller. 

429 if "" in version.split("."): 

430 raise InvalidVersion(f"Invalid version: {version!r}") from None 

431 raise 

432 

433 self._epoch = 0 

434 self._pre = None 

435 self._post = None 

436 self._dev = None 

437 self._local = None 

438 self._key_cache = None 

439 self._hash_cache = None 

440 return 

441 

442 # Validate the version and parse it into pieces 

443 try: 

444 match = self._regex.fullmatch(version) 

445 except TypeError: 

446 raise InvalidVersion(f"Invalid version: {version!r}") from None 

447 if not match: 

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

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

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

451 # We can type ignore the assignments below because the regex guarantees 

452 # the correct strings 

453 self._pre = _parse_letter_version(match.group("pre_l"), match.group("pre_n")) # type: ignore[assignment] 

454 self._post = _parse_letter_version( # type: ignore[assignment] 

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

456 ) 

457 self._dev = _parse_letter_version(match.group("dev_l"), match.group("dev_n")) # type: ignore[assignment] 

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

459 

460 # Key which will be used for sorting 

461 self._key_cache = None 

462 self._hash_cache = None 

463 

464 @classmethod 

465 def from_parts( 

466 cls, 

467 *, 

468 epoch: int = 0, 

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

470 pre: tuple[str, int] | None = None, 

471 post: int | None = None, 

472 dev: int | None = None, 

473 local: str | None = None, 

474 ) -> Self: 

475 """ 

476 Return a new version composed of the various parts. 

477 

478 This allows you to build a version without going though a string and 

479 running a regular expression. It normalizes pre-release strings. The 

480 ``release=`` keyword argument is required. 

481 

482 >>> Version.from_parts(release=(1,2,3)) 

483 <Version('1.2.3')> 

484 >>> Version.from_parts(release=(0,1,0), pre=("b", 1)) 

485 <Version('0.1.0b1')> 

486 

487 :param epoch: 

488 :param release: This version tuple is required 

489 

490 .. versionadded:: 26.1 

491 """ 

492 _epoch = _validate_epoch(epoch) 

493 _release = _validate_release(release) 

494 _pre = _validate_pre(pre) if pre is not None else None 

495 _post = _validate_post(post) if post is not None else None 

496 _dev = _validate_dev(dev) if dev is not None else None 

497 _local = _validate_local(local) if local is not None else None 

498 

499 new_version = cls.__new__(cls) 

500 new_version._key_cache = None 

501 new_version._hash_cache = None 

502 new_version._epoch = _epoch 

503 new_version._release = _release 

504 new_version._pre = _pre 

505 new_version._post = _post 

506 new_version._dev = _dev 

507 new_version._local = _local 

508 

509 return new_version 

510 

511 def __replace__( 

512 self, 

513 *, 

514 epoch: int | None = _UNSET, 

515 release: tuple[int, ...] | None = _UNSET, 

516 pre: tuple[str, int] | None = _UNSET, 

517 post: int | None = _UNSET, 

518 dev: int | None = _UNSET, 

519 local: str | None = _UNSET, 

520 ) -> Self: 

521 """ 

522 __replace__(*, epoch=..., release=..., pre=..., post=..., dev=..., local=...) 

523 

524 Return a new version with parts replaced. 

525 

526 This returns a new version (unless no parts were changed). The 

527 pre-release is normalized. Setting a value to ``None`` clears it. 

528 

529 >>> v = Version("1.2.3") 

530 >>> v.__replace__(pre=("a", 1)) 

531 <Version('1.2.3a1')> 

532 

533 :param int | None epoch: 

534 :param tuple[int, ...] | None release: 

535 :param tuple[str, int] | None pre: 

536 :param int | None post: 

537 :param int | None dev: 

538 :param str | None local: 

539 

540 .. versionadded:: 26.0 

541 .. versionchanged:: 26.1 

542 

543 The pre-release portion is now normalized. 

544 

545 .. versionchanged:: 26.4 

546 

547 Unknown keyword arguments now raise :exc:`TypeError` instead of 

548 being silently ignored. 

549 """ 

550 _epoch = self._epoch if epoch is _UNSET else _validate_epoch(epoch) 

551 _release = self._release if release is _UNSET else _validate_release(release) 

552 _pre = self._pre if pre is _UNSET else _validate_pre(pre) 

553 _post = self._post if post is _UNSET else _validate_post(post) 

554 _dev = self._dev if dev is _UNSET else _validate_dev(dev) 

555 _local = self._local if local is _UNSET else _validate_local(local) 

556 

557 if ( 

558 _epoch == self._epoch 

559 and _release == self._release 

560 and _pre == self._pre 

561 and _post == self._post 

562 and _dev == self._dev 

563 and _local == self._local 

564 ): 

565 return self 

566 

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

568 new_version._key_cache = None 

569 new_version._hash_cache = None 

570 new_version._epoch = _epoch 

571 new_version._release = _release 

572 new_version._pre = _pre 

573 new_version._post = _post 

574 new_version._dev = _dev 

575 new_version._local = _local 

576 

577 return new_version 

578 

579 @property 

580 def _key(self) -> CmpKey: 

581 if self._key_cache is None: 

582 self._key_cache = _cmpkey( 

583 self._epoch, 

584 self._release, 

585 self._pre, 

586 self._post, 

587 self._dev, 

588 self._local, 

589 ) 

590 return self._key_cache 

591 

592 # __hash__ must be defined when __eq__ is overridden, 

593 # otherwise Python sets __hash__ to None. 

594 def __hash__(self) -> int: 

595 if (cached_hash := self._hash_cache) is not None: 

596 return cached_hash 

597 

598 if (key := self._key_cache) is None: 

599 self._key_cache = key = _cmpkey( 

600 self._epoch, 

601 self._release, 

602 self._pre, 

603 self._post, 

604 self._dev, 

605 self._local, 

606 ) 

607 self._hash_cache = cached_hash = hash(key) 

608 return cached_hash 

609 

610 # Override comparison methods to use direct _key_cache access 

611 # This is faster than property access, especially before Python 3.12 

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

613 if isinstance(other, Version): 

614 if self._key_cache is None: 

615 self._key_cache = _cmpkey( 

616 self._epoch, 

617 self._release, 

618 self._pre, 

619 self._post, 

620 self._dev, 

621 self._local, 

622 ) 

623 if other._key_cache is None: 

624 other._key_cache = _cmpkey( 

625 other._epoch, 

626 other._release, 

627 other._pre, 

628 other._post, 

629 other._dev, 

630 other._local, 

631 ) 

632 return self._key_cache < other._key_cache 

633 

634 if not isinstance(other, _BaseVersion): 

635 return NotImplemented 

636 

637 return super().__lt__(other) 

638 

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

640 if isinstance(other, Version): 

641 if self._key_cache is None: 

642 self._key_cache = _cmpkey( 

643 self._epoch, 

644 self._release, 

645 self._pre, 

646 self._post, 

647 self._dev, 

648 self._local, 

649 ) 

650 if other._key_cache is None: 

651 other._key_cache = _cmpkey( 

652 other._epoch, 

653 other._release, 

654 other._pre, 

655 other._post, 

656 other._dev, 

657 other._local, 

658 ) 

659 return self._key_cache <= other._key_cache 

660 

661 if not isinstance(other, _BaseVersion): 

662 return NotImplemented 

663 

664 return super().__le__(other) 

665 

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

667 if isinstance(other, Version): 

668 if self._key_cache is None: 

669 self._key_cache = _cmpkey( 

670 self._epoch, 

671 self._release, 

672 self._pre, 

673 self._post, 

674 self._dev, 

675 self._local, 

676 ) 

677 if other._key_cache is None: 

678 other._key_cache = _cmpkey( 

679 other._epoch, 

680 other._release, 

681 other._pre, 

682 other._post, 

683 other._dev, 

684 other._local, 

685 ) 

686 return self._key_cache == other._key_cache 

687 

688 if not isinstance(other, _BaseVersion): 

689 return NotImplemented 

690 

691 return super().__eq__(other) 

692 

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

694 if isinstance(other, Version): 

695 if self._key_cache is None: 

696 self._key_cache = _cmpkey( 

697 self._epoch, 

698 self._release, 

699 self._pre, 

700 self._post, 

701 self._dev, 

702 self._local, 

703 ) 

704 if other._key_cache is None: 

705 other._key_cache = _cmpkey( 

706 other._epoch, 

707 other._release, 

708 other._pre, 

709 other._post, 

710 other._dev, 

711 other._local, 

712 ) 

713 return self._key_cache >= other._key_cache 

714 

715 if not isinstance(other, _BaseVersion): 

716 return NotImplemented 

717 

718 return super().__ge__(other) 

719 

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

721 if isinstance(other, Version): 

722 if self._key_cache is None: 

723 self._key_cache = _cmpkey( 

724 self._epoch, 

725 self._release, 

726 self._pre, 

727 self._post, 

728 self._dev, 

729 self._local, 

730 ) 

731 if other._key_cache is None: 

732 other._key_cache = _cmpkey( 

733 other._epoch, 

734 other._release, 

735 other._pre, 

736 other._post, 

737 other._dev, 

738 other._local, 

739 ) 

740 return self._key_cache > other._key_cache 

741 

742 if not isinstance(other, _BaseVersion): 

743 return NotImplemented 

744 

745 return super().__gt__(other) 

746 

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

748 if isinstance(other, Version): 

749 if self._key_cache is None: 

750 self._key_cache = _cmpkey( 

751 self._epoch, 

752 self._release, 

753 self._pre, 

754 self._post, 

755 self._dev, 

756 self._local, 

757 ) 

758 if other._key_cache is None: 

759 other._key_cache = _cmpkey( 

760 other._epoch, 

761 other._release, 

762 other._pre, 

763 other._post, 

764 other._dev, 

765 other._local, 

766 ) 

767 return self._key_cache != other._key_cache 

768 

769 if not isinstance(other, _BaseVersion): 

770 return NotImplemented 

771 

772 return super().__ne__(other) 

773 

774 def __getstate__( 

775 self, 

776 ) -> tuple[ 

777 int, 

778 tuple[int, ...], 

779 tuple[str, int] | None, 

780 tuple[str, int] | None, 

781 tuple[str, int] | None, 

782 LocalType | None, 

783 ]: 

784 # Return state as a 6-item tuple for compactness: 

785 # (epoch, release, pre, post, dev, local) 

786 # Cache members are excluded and will be recomputed on demand 

787 return ( 

788 self._epoch, 

789 self._release, 

790 self._pre, 

791 self._post, 

792 self._dev, 

793 self._local, 

794 ) 

795 

796 def __setstate__(self, state: object) -> None: 

797 # Always discard cached values — they may contain stale references 

798 # (e.g. packaging._structures.InfinityType from pre-26.1 pickles) 

799 # and will be recomputed on demand from the core fields above. 

800 self._key_cache = None 

801 self._hash_cache = None 

802 

803 if isinstance(state, tuple): 

804 if len(state) == 6: 

805 # New format (26.2+): (epoch, release, pre, post, dev, local) 

806 ( 

807 self._epoch, 

808 self._release, 

809 self._pre, 

810 self._post, 

811 self._dev, 

812 self._local, 

813 ) = state 

814 return 

815 if len(state) == 2: 

816 # Format (packaging 26.0-26.1): (None, {slot: value}). 

817 _, slot_dict = state 

818 if isinstance(slot_dict, dict): 

819 self._epoch = slot_dict["_epoch"] 

820 self._release = slot_dict["_release"] 

821 self._pre = slot_dict.get("_pre") 

822 self._post = slot_dict.get("_post") 

823 self._dev = slot_dict.get("_dev") 

824 self._local = slot_dict.get("_local") 

825 return 

826 if isinstance(state, dict): 

827 # Old format (packaging <= 25.x, no __slots__): state is a plain 

828 # dict with "_version" (_Version NamedTuple) and "_key" entries. 

829 version_nt = state.get("_version") 

830 if version_nt is not None: 

831 self._epoch = version_nt.epoch 

832 self._release = version_nt.release 

833 self._pre = version_nt.pre 

834 self._post = version_nt.post 

835 self._dev = version_nt.dev 

836 self._local = version_nt.local 

837 return 

838 

839 raise TypeError(f"Cannot restore Version from {state!r}") 

840 

841 @property 

842 @_deprecated("Version._version is private and will be removed soon") 

843 def _version(self) -> _Version: 

844 return _Version( 

845 self._epoch, self._release, self._dev, self._pre, self._post, self._local 

846 ) 

847 

848 @_version.setter 

849 @_deprecated("Version._version is private and will be removed soon") 

850 def _version(self, value: _Version) -> None: 

851 self._epoch = value.epoch 

852 self._release = value.release 

853 self._dev = value.dev 

854 self._pre = value.pre 

855 self._post = value.post 

856 self._local = value.local 

857 self._key_cache = None 

858 self._hash_cache = None 

859 

860 def __repr__(self) -> str: 

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

862 

863 >>> Version('1.0.0') 

864 <Version('1.0.0')> 

865 """ 

866 return f"<{self.__class__.__name__}({str(self)!r})>" 

867 

868 def __str__(self) -> str: 

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

870 

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

872 '1.0a5' 

873 """ 

874 # This is a hot function, so not calling self.base_version 

875 version = ".".join(map(str, self.release)) 

876 

877 # Epoch 

878 if self.epoch: 

879 version = f"{self.epoch}!{version}" 

880 

881 # Pre-release 

882 if self.pre is not None: 

883 version += "".join(map(str, self.pre)) 

884 

885 # Post-release 

886 if self.post is not None: 

887 version += f".post{self.post}" 

888 

889 # Development release 

890 if self.dev is not None: 

891 version += f".dev{self.dev}" 

892 

893 # Local version segment 

894 if self.local is not None: 

895 version += f"+{self.local}" 

896 

897 return version 

898 

899 @property 

900 def _str(self) -> str: 

901 """Internal property for match_args""" 

902 return str(self) 

903 

904 @property 

905 def epoch(self) -> int: 

906 """The epoch of the version. 

907 

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

909 0 

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

911 1 

912 """ 

913 return self._epoch 

914 

915 @property 

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

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

918 

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

920 (1, 2, 3) 

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

922 (2, 0, 0) 

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

924 (2, 0, 0) 

925 

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

927 post-release suffixes. 

928 """ 

929 return self._release 

930 

931 @property 

932 def pre(self) -> tuple[Literal["a", "b", "rc"], int] | None: 

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

934 

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

936 None 

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

938 ('a', 1) 

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

940 ('b', 1) 

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

942 ('rc', 1) 

943 """ 

944 return self._pre 

945 

946 @property 

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

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

949 

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

951 None 

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

953 1 

954 """ 

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

956 

957 @property 

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

959 """The development number of the version. 

960 

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

962 None 

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

964 1 

965 """ 

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

967 

968 @property 

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

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

971 

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

973 None 

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

975 'abc' 

976 """ 

977 if self._local: 

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

979 else: 

980 return None 

981 

982 @property 

983 def public(self) -> str: 

984 """The public portion of the version. 

985 

986 This returns a string. If you want a :class:`Version` again and care 

987 about performance, use ``v.__replace__(local=None)`` instead. 

988 

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

990 '1.2.3' 

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

992 '1.2.3' 

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

994 '1!1.2.3.dev1' 

995 """ 

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

997 

998 @property 

999 def base_version(self) -> str: 

1000 """The "base version" of the version. 

1001 

1002 This returns a string. If you want a :class:`Version` again and care 

1003 about performance, use 

1004 ``v.__replace__(pre=None, post=None, dev=None, local=None)`` instead. 

1005 

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

1007 '1.2.3' 

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

1009 '1.2.3' 

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

1011 '1!1.2.3' 

1012 

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

1014 release markers. 

1015 """ 

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

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

1018 

1019 @property 

1020 def is_prerelease(self) -> bool: 

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

1022 

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

1024 False 

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

1026 True 

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

1028 True 

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

1030 True 

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

1032 True 

1033 """ 

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

1035 

1036 @property 

1037 def is_postrelease(self) -> bool: 

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

1039 

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

1041 False 

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

1043 True 

1044 """ 

1045 return self.post is not None 

1046 

1047 @property 

1048 def is_devrelease(self) -> bool: 

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

1050 

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

1052 False 

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

1054 True 

1055 """ 

1056 return self.dev is not None 

1057 

1058 @property 

1059 def major(self) -> int: 

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

1061 

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

1063 1 

1064 

1065 .. versionadded:: 20.0 

1066 """ 

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

1068 

1069 @property 

1070 def minor(self) -> int: 

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

1072 

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

1074 2 

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

1076 0 

1077 

1078 .. versionadded:: 20.0 

1079 """ 

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

1081 

1082 @property 

1083 def micro(self) -> int: 

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

1085 

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

1087 3 

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

1089 0 

1090 

1091 .. versionadded:: 20.0 

1092 """ 

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

1094 

1095 

1096class _TrimmedRelease(Version): 

1097 __slots__ = () 

1098 

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

1100 if isinstance(version, Version): 

1101 self._epoch = version._epoch 

1102 self._release = version._release 

1103 self._dev = version._dev 

1104 self._pre = version._pre 

1105 self._post = version._post 

1106 self._local = version._local 

1107 self._key_cache = version._key_cache 

1108 self._hash_cache = version._hash_cache 

1109 return 

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

1111 

1112 @property 

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

1114 """ 

1115 Release segment without any trailing zeros. 

1116 

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

1118 (1,) 

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

1120 (0,) 

1121 """ 

1122 # This leaves one 0. 

1123 rel = super().release 

1124 len_release = len(rel) 

1125 i = len_release 

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

1127 i -= 1 

1128 return rel if i == len_release else rel[:i] 

1129 

1130 

1131def _parse_letter_version( 

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

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

1134 if letter: 

1135 # We normalize any letters to their lower case form 

1136 letter = letter.lower() 

1137 

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

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

1140 # spelling. 

1141 letter = _LETTER_NORMALIZATION.get(letter, letter) 

1142 

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

1144 # not a numeral associated with it. 

1145 return letter, int(number or 0) 

1146 

1147 if number: 

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

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

1150 return "post", int(number) 

1151 

1152 return None 

1153 

1154 

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

1156 

1157 

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

1159 """ 

1160 Takes a string like ``"abc.1.twelve"`` and turns it into 

1161 ``("abc", 1, "twelve")``. 

1162 """ 

1163 if local is not None: 

1164 return tuple( 

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

1166 for part in _local_version_separators.split(local) 

1167 ) 

1168 return None 

1169 

1170 

1171# Sort ranks for pre-release: dev-only < a < b < rc < stable (no pre-release). 

1172_PRE_RANK = {"a": 0, "b": 1, "rc": 2} 

1173_PRE_RANK_DEV_ONLY = -1 # sorts before a(0) 

1174_PRE_RANK_STABLE = 3 # sorts after rc(2) 

1175 

1176# In local version segments, strings sort before ints per PEP 440. 

1177_LOCAL_STR_RANK = -1 # sorts before all non-negative ints 

1178 

1179# Pre-computed suffix for stable releases (no pre, post, or dev segments). 

1180# See _cmpkey() for the suffix layout. 

1181_STABLE_SUFFIX = (_PRE_RANK_STABLE, 0, 0, 0, 1, 0) 

1182 

1183 

1184def _cmpkey( 

1185 epoch: int, 

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

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

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

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

1190 local: LocalType | None, 

1191) -> CmpKey: 

1192 """Build a comparison key for PEP 440 ordering. 

1193 

1194 Returns ``(epoch, release, suffix)`` or 

1195 ``(epoch, release, suffix, local)`` so that plain tuple 

1196 comparison gives the correct order. 

1197 

1198 Trailing zeros are stripped from the release so that ``1.0.0 == 1``. 

1199 

1200 The suffix is a flat 6-int tuple that encodes pre/post/dev: 

1201 ``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)`` 

1202 

1203 pre_rank: dev-only=-1, a=0, b=1, rc=2, no-pre=3 

1204 Dev-only releases (no pre or post) get -1 so they sort before 

1205 any alpha/beta/rc. Releases without a pre-release tag get 3 

1206 so they sort after rc. 

1207 post_rank: no-post=0, post=1 

1208 Releases without a post segment sort before those with one. 

1209 dev_rank: dev=0, no-dev=1 

1210 Releases without a dev segment sort after those with one. 

1211 

1212 Local segments use ``(n, "")`` for ints and ``(-1, s)`` for strings, 

1213 following PEP 440: strings sort before ints, strings compare 

1214 lexicographically, ints compare numerically, and shorter segments 

1215 sort before longer when prefixes match. Versions without a local 

1216 segment sort before those with one (3-tuple < 4-tuple). 

1217 

1218 >>> _cmpkey(0, (1, 0, 0), None, None, None, None) 

1219 (0, (1,), (3, 0, 0, 0, 1, 0)) 

1220 >>> _cmpkey(0, (1,), ("a", 1), None, None, None) 

1221 (0, (1,), (0, 1, 0, 0, 1, 0)) 

1222 >>> _cmpkey(0, (1,), None, None, None, ("ubuntu", 1)) 

1223 (0, (1,), (3, 0, 0, 0, 1, 0), ((-1, 'ubuntu'), (1, ''))) 

1224 """ 

1225 # Strip trailing zeros: 1.0.0 compares equal to 1. 

1226 len_release = len(release) 

1227 i = len_release 

1228 while i and release[i - 1] == 0: 

1229 i -= 1 

1230 trimmed = release if i == len_release else release[:i] 

1231 

1232 # Fast path: stable release with no local segment. 

1233 if pre is None and post is None and dev is None and local is None: 

1234 return epoch, trimmed, _STABLE_SUFFIX 

1235 

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

1237 # dev-only (e.g. 1.0.dev1) sorts before all pre-releases. 

1238 pre_rank, pre_n = _PRE_RANK_DEV_ONLY, 0 

1239 elif pre is None: 

1240 pre_rank, pre_n = _PRE_RANK_STABLE, 0 

1241 else: 

1242 pre_rank, pre_n = _PRE_RANK[pre[0]], pre[1] 

1243 

1244 post_rank = 0 if post is None else 1 

1245 post_n = 0 if post is None else post[1] 

1246 

1247 dev_rank = 1 if dev is None else 0 

1248 dev_n = 0 if dev is None else dev[1] 

1249 

1250 suffix = (pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n) 

1251 

1252 if local is None: 

1253 return epoch, trimmed, suffix 

1254 

1255 cmp_local: CmpLocalType = tuple( 

1256 (seg, "") if isinstance(seg, int) else (_LOCAL_STR_RANK, seg) for seg in local 

1257 ) 

1258 return epoch, trimmed, suffix, cmp_local