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
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
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::
7 from packaging.version import parse, Version
8"""
10from __future__ import annotations
12import re
13import sys
14import typing
15from typing import Any, Callable, Literal, SupportsInt, Tuple, TypedDict, Union
17from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
19if typing.TYPE_CHECKING:
20 from typing_extensions import Self, Unpack
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}
32__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"]
34LocalType = Tuple[Union[int, str], ...]
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]
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
61def parse(version: str) -> Version:
62 """Parse the given version string.
64 >>> parse('1.0.dev1')
65 <Version('1.0.dev1')>
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)
73class InvalidVersion(ValueError):
74 """Raised when a version string is not a valid version.
76 >>> Version("invalid")
77 Traceback (most recent call last):
78 ...
79 packaging.version.InvalidVersion: Invalid version: 'invalid'
80 """
83class _BaseVersion:
84 __slots__ = ()
86 @property
87 def _key(self) -> tuple[Any, ...]:
88 raise NotImplementedError # pragma: no cover
90 def __hash__(self) -> int:
91 return hash(self._key)
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
100 return self._key < other._key
102 def __le__(self, other: _BaseVersion) -> bool:
103 if not isinstance(other, _BaseVersion):
104 return NotImplemented
106 return self._key <= other._key
108 def __eq__(self, other: object) -> bool:
109 if not isinstance(other, _BaseVersion):
110 return NotImplemented
112 return self._key == other._key
114 def __ge__(self, other: _BaseVersion) -> bool:
115 if not isinstance(other, _BaseVersion):
116 return NotImplemented
118 return self._key >= other._key
120 def __gt__(self, other: _BaseVersion) -> bool:
121 if not isinstance(other, _BaseVersion):
122 return NotImplemented
124 return self._key > other._key
126 def __ne__(self, other: object) -> bool:
127 if not isinstance(other, _BaseVersion):
128 return NotImplemented
130 return self._key != other._key
133# Deliberately not anchored to the start and end of the string, to make it
134# easier for 3rd party code to reuse
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"""
173_VERSION_PATTERN_OLD = _VERSION_PATTERN.replace("*+", "*").replace("?+", "?")
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.
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.
186:meta hide-value:
187"""
190# Validation pattern for local version in replace()
191_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE)
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)
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)
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)
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)
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)
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)
256class Version(_BaseVersion):
257 """This class abstracts handling of a project's versions.
259 A :class:`Version` instance is comparison aware and can be compared and
260 sorted using the standard Python interfaces.
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 """
280 __slots__ = ("_dev", "_epoch", "_key_cache", "_local", "_post", "_pre", "_release")
281 __match_args__ = ("_str",)
283 _regex = re.compile(r"\s*" + VERSION_PATTERN + r"\s*", re.VERBOSE | re.IGNORECASE)
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
292 _key_cache: CmpKey | None
294 def __init__(self, version: str) -> None:
295 """Initialize a Version object.
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"))
317 # Key which will be used for sorting
318 self._key_cache = None
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
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
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
351 return new_version
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
366 def __repr__(self) -> str:
367 """A representation of the Version that shows all internal state.
369 >>> Version('1.0.0')
370 <Version('1.0.0')>
371 """
372 return f"<Version('{self}')>"
374 def __str__(self) -> str:
375 """A string representation of the version that can be round-tripped.
377 >>> str(Version("1.0a5"))
378 '1.0a5'
379 """
380 parts = [self.base_version]
382 # Pre-release
383 if self.pre is not None:
384 parts.append("".join(map(str, self.pre)))
386 # Post-release
387 if self.post is not None:
388 parts.append(f".post{self.post}")
390 # Development release
391 if self.dev is not None:
392 parts.append(f".dev{self.dev}")
394 # Local version segment
395 if self.local is not None:
396 parts.append(f"+{self.local}")
398 return "".join(parts)
400 @property
401 def _str(self) -> str:
402 """Internal property for match_args"""
403 return str(self)
405 @property
406 def epoch(self) -> int:
407 """The epoch of the version.
409 >>> Version("2.0.0").epoch
410 0
411 >>> Version("1!2.0.0").epoch
412 1
413 """
414 return self._epoch
416 @property
417 def release(self) -> tuple[int, ...]:
418 """The components of the "release" segment of the version.
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)
427 Includes trailing zeroes but not the epoch or any pre-release / development /
428 post-release suffixes.
429 """
430 return self._release
432 @property
433 def pre(self) -> tuple[str, int] | None:
434 """The pre-release segment of the version.
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
447 @property
448 def post(self) -> int | None:
449 """The post-release number of the version.
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
458 @property
459 def dev(self) -> int | None:
460 """The development number of the version.
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
469 @property
470 def local(self) -> str | None:
471 """The local version segment of the version.
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
483 @property
484 def public(self) -> str:
485 """The public portion of the version.
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]
496 @property
497 def base_version(self) -> str:
498 """The "base version" of the version.
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'
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
513 @property
514 def is_prerelease(self) -> bool:
515 """Whether this version is a pre-release.
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
530 @property
531 def is_postrelease(self) -> bool:
532 """Whether this version is a post-release.
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
541 @property
542 def is_devrelease(self) -> bool:
543 """Whether this version is a development release.
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
552 @property
553 def major(self) -> int:
554 """The first item of :attr:`release` or ``0`` if unavailable.
556 >>> Version("1.2.3").major
557 1
558 """
559 return self.release[0] if len(self.release) >= 1 else 0
561 @property
562 def minor(self) -> int:
563 """The second item of :attr:`release` or ``0`` if unavailable.
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
572 @property
573 def micro(self) -> int:
574 """The third item of :attr:`release` or ``0`` if unavailable.
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
584class _TrimmedRelease(Version):
585 __slots__ = ()
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
599 @property
600 def release(self) -> tuple[int, ...]:
601 """
602 Release segment without any trailing zeros.
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]
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()
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)
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)
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)
638 return None
641_local_version_separators = re.compile(r"[\._-]")
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
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 ()
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)
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
692 # Versions without a post segment should sort before those with one.
693 if post is None:
694 _post: CmpPrePostDevType = NegativeInfinity
696 else:
697 _post = post
699 # Versions without a development segment should sort after those with one.
700 if dev is None:
701 _dev: CmpPrePostDevType = Infinity
703 else:
704 _dev = dev
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 )
721 return epoch, _release, _pre, _post, _dev, _local