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.
7 from packaging.version import parse, Version
13 from typing
import Any
, Callable
, Optional
, SupportsInt
, Tuple
, Union
15 from ._structures
import Infinity
, InfinityType
, NegativeInfinity
, NegativeInfinityType
17 __all__
= ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"]
19 InfiniteTypes
= Union
[InfinityType
, NegativeInfinityType
]
20 PrePostDevType
= Union
[InfiniteTypes
, Tuple
[str, int]]
21 SubLocalType
= Union
[InfiniteTypes
, int, str]
27 Tuple
[SubLocalType
, str],
28 Tuple
[NegativeInfinityType
, SubLocalType
],
34 int, Tuple
[int, ...], PrePostDevType
, PrePostDevType
, PrePostDevType
, LocalType
36 VersionComparisonMethod
= Callable
[[CmpKey
, CmpKey
], bool]
38 _Version
= collections
.namedtuple(
39 "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
43 def parse(version
: str) -> "Version":
44 """Parse the given version string.
49 :param version: The version string to parse.
50 :raises InvalidVersion: When the version string is not a valid version.
52 return Version(version
)
55 class InvalidVersion(ValueError):
56 """Raised when a version string is not a valid version.
58 >>> Version("invalid")
59 Traceback (most recent call last):
61 packaging.version.InvalidVersion: Invalid version: 'invalid'
68 def __hash__(self
) -> int:
69 return hash(self
._key
)
71 # Please keep the duplicated `isinstance` check
72 # in the six comparisons hereunder
73 # unless you find a way to avoid adding overhead function calls.
74 def __lt__(self
, other
: "_BaseVersion") -> bool:
75 if not isinstance(other
, _BaseVersion
):
78 return self
._key
< other
._key
80 def __le__(self
, other
: "_BaseVersion") -> bool:
81 if not isinstance(other
, _BaseVersion
):
84 return self
._key
<= other
._key
86 def __eq__(self
, other
: object) -> bool:
87 if not isinstance(other
, _BaseVersion
):
90 return self
._key
== other
._key
92 def __ge__(self
, other
: "_BaseVersion") -> bool:
93 if not isinstance(other
, _BaseVersion
):
96 return self
._key
>= other
._key
98 def __gt__(self
, other
: "_BaseVersion") -> bool:
99 if not isinstance(other
, _BaseVersion
):
100 return NotImplemented
102 return self
._key
> other
._key
104 def __ne__(self
, other
: object) -> bool:
105 if not isinstance(other
, _BaseVersion
):
106 return NotImplemented
108 return self
._key
!= other
._key
111 # Deliberately not anchored to the start and end of the string, to make it
112 # easier for 3rd party code to reuse
113 _VERSION_PATTERN
= r
"""
116 (?:(?P<epoch>[0-9]+)!)? # epoch
117 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
118 (?P<pre> # pre-release
120 (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
124 (?P<post> # post release
125 (?:-(?P<post_n1>[0-9]+))
129 (?P<post_l>post|rev|r)
134 (?P<dev> # dev release
141 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
144 VERSION_PATTERN
= _VERSION_PATTERN
146 A string containing the regular expression used to match a valid version.
148 The pattern is not anchored at either end, and is intended for embedding in larger
149 expressions (for example, matching a version number as part of a file name). The
150 regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
157 class Version(_BaseVersion
):
158 """This class abstracts handling of a project's versions.
160 A :class:`Version` instance is comparison aware and can be compared and
161 sorted using the standard Python interfaces.
163 >>> v1 = Version("1.0a5")
164 >>> v2 = Version("1.0")
181 _regex
= re
.compile(r
"^\s*" + VERSION_PATTERN
+ r
"\s*$", re
.VERBOSE | re
.IGNORECASE
)
184 def __init__(self
, version
: str) -> None:
185 """Initialize a Version object.
188 The string representation of a version which will be parsed and normalized
190 :raises InvalidVersion:
191 If the ``version`` does not conform to PEP 440 in any way then this
192 exception will be raised.
195 # Validate the version and parse it into pieces
196 match
= self
._regex
.search(version
)
198 raise InvalidVersion(f
"Invalid version: '{version}'")
200 # Store the parsed out pieces of the version
201 self
._version
= _Version(
202 epoch
=int(match
.group("epoch")) if match
.group("epoch") else 0,
203 release
=tuple(int(i
) for i
in match
.group("release").split(".")),
204 pre
=_parse_letter_version(match
.group("pre_l"), match
.group("pre_n")),
205 post
=_parse_letter_version(
206 match
.group("post_l"), match
.group("post_n1") or match
.group("post_n2")
208 dev
=_parse_letter_version(match
.group("dev_l"), match
.group("dev_n")),
209 local
=_parse_local_version(match
.group("local")),
212 # Generate a key which will be used for sorting
215 self
._version
.release
,
222 def __repr__(self
) -> str:
223 """A representation of the Version that shows all internal state.
228 return f
"<Version('{self}')>"
230 def __str__(self
) -> str:
231 """A string representation of the version that can be rounded-tripped.
233 >>> str(Version("1.0a5"))
240 parts
.append(f
"{self.epoch}!")
243 parts
.append(".".join(str(x
) for x
in self
.release
))
246 if self
.pre
is not None:
247 parts
.append("".join(str(x
) for x
in self
.pre
))
250 if self
.post
is not None:
251 parts
.append(f
".post{self.post}")
253 # Development release
254 if self
.dev
is not None:
255 parts
.append(f
".dev{self.dev}")
257 # Local version segment
258 if self
.local
is not None:
259 parts
.append(f
"+{self.local}")
261 return "".join(parts
)
264 def epoch(self
) -> int:
265 """The epoch of the version.
267 >>> Version("2.0.0").epoch
269 >>> Version("1!2.0.0").epoch
272 _epoch
: int = self
._version
.epoch
276 def release(self
) -> Tuple
[int, ...]:
277 """The components of the "release" segment of the version.
279 >>> Version("1.2.3").release
281 >>> Version("2.0.0").release
283 >>> Version("1!2.0.0.post0").release
286 Includes trailing zeroes but not the epoch or any pre-release / development /
287 post-release suffixes.
289 _release
: Tuple
[int, ...] = self
._version
.release
293 def pre(self
) -> Optional
[Tuple
[str, int]]:
294 """The pre-release segment of the version.
296 >>> print(Version("1.2.3").pre)
298 >>> Version("1.2.3a1").pre
300 >>> Version("1.2.3b1").pre
302 >>> Version("1.2.3rc1").pre
305 _pre
: Optional
[Tuple
[str, int]] = self
._version
.pre
309 def post(self
) -> Optional
[int]:
310 """The post-release number of the version.
312 >>> print(Version("1.2.3").post)
314 >>> Version("1.2.3.post1").post
317 return self
._version
.post
[1] if self
._version
.post
else None
320 def dev(self
) -> Optional
[int]:
321 """The development number of the version.
323 >>> print(Version("1.2.3").dev)
325 >>> Version("1.2.3.dev1").dev
328 return self
._version
.dev
[1] if self
._version
.dev
else None
331 def local(self
) -> Optional
[str]:
332 """The local version segment of the version.
334 >>> print(Version("1.2.3").local)
336 >>> Version("1.2.3+abc").local
339 if self
._version
.local
:
340 return ".".join(str(x
) for x
in self
._version
.local
)
345 def public(self
) -> str:
346 """The public portion of the version.
348 >>> Version("1.2.3").public
350 >>> Version("1.2.3+abc").public
352 >>> Version("1.2.3+abc.dev1").public
355 return str(self
).split("+", 1)[0]
358 def base_version(self
) -> str:
359 """The "base version" of the version.
361 >>> Version("1.2.3").base_version
363 >>> Version("1.2.3+abc").base_version
365 >>> Version("1!1.2.3+abc.dev1").base_version
368 The "base version" is the public version of the project without any pre or post
375 parts
.append(f
"{self.epoch}!")
378 parts
.append(".".join(str(x
) for x
in self
.release
))
380 return "".join(parts
)
383 def is_prerelease(self
) -> bool:
384 """Whether this version is a pre-release.
386 >>> Version("1.2.3").is_prerelease
388 >>> Version("1.2.3a1").is_prerelease
390 >>> Version("1.2.3b1").is_prerelease
392 >>> Version("1.2.3rc1").is_prerelease
394 >>> Version("1.2.3dev1").is_prerelease
397 return self
.dev
is not None or self
.pre
is not None
400 def is_postrelease(self
) -> bool:
401 """Whether this version is a post-release.
403 >>> Version("1.2.3").is_postrelease
405 >>> Version("1.2.3.post1").is_postrelease
408 return self
.post
is not None
411 def is_devrelease(self
) -> bool:
412 """Whether this version is a development release.
414 >>> Version("1.2.3").is_devrelease
416 >>> Version("1.2.3.dev1").is_devrelease
419 return self
.dev
is not None
422 def major(self
) -> int:
423 """The first item of :attr:`release` or ``0`` if unavailable.
425 >>> Version("1.2.3").major
428 return self
.release
[0] if len(self
.release
) >= 1 else 0
431 def minor(self
) -> int:
432 """The second item of :attr:`release` or ``0`` if unavailable.
434 >>> Version("1.2.3").minor
436 >>> Version("1").minor
439 return self
.release
[1] if len(self
.release
) >= 2 else 0
442 def micro(self
) -> int:
443 """The third item of :attr:`release` or ``0`` if unavailable.
445 >>> Version("1.2.3").micro
447 >>> Version("1").micro
450 return self
.release
[2] if len(self
.release
) >= 3 else 0
453 def _parse_letter_version(
454 letter
: str, number
: Union
[str, bytes, SupportsInt
]
455 ) -> Optional
[Tuple
[str, int]]:
458 # We consider there to be an implicit 0 in a pre-release if there is
459 # not a numeral associated with it.
463 # We normalize any letters to their lower case form
464 letter
= letter
.lower()
466 # We consider some words to be alternate spellings of other words and
467 # in those cases we want to normalize the spellings to our preferred
469 if letter
== "alpha":
471 elif letter
== "beta":
473 elif letter
in ["c", "pre", "preview"]:
475 elif letter
in ["rev", "r"]:
478 return letter
, int(number
)
479 if not letter
and number
:
480 # We assume if we are given a number, but we are not given a letter
481 # then this is using the implicit post release syntax (e.g. 1.0-1)
484 return letter
, int(number
)
489 _local_version_separators
= re
.compile(r
"[\._-]")
492 def _parse_local_version(local
: str) -> Optional
[LocalType
]:
494 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
496 if local
is not None:
498 part
.lower() if not part
.isdigit() else int(part
)
499 for part
in _local_version_separators
.split(local
)
506 release
: Tuple
[int, ...],
507 pre
: Optional
[Tuple
[str, int]],
508 post
: Optional
[Tuple
[str, int]],
509 dev
: Optional
[Tuple
[str, int]],
510 local
: Optional
[Tuple
[SubLocalType
]],
513 # When we compare a release version, we want to compare it with all of the
514 # trailing zeros removed. So we'll use a reverse the list, drop all the now
515 # leading zeros until we come to something non zero, then take the rest
516 # re-reverse it back into the correct order and make it a tuple and use
517 # that for our sorting key.
519 reversed(list(itertools
.dropwhile(lambda x
: x
== 0, reversed(release
))))
522 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
523 # We'll do this by abusing the pre segment, but we _only_ want to do this
524 # if there is not a pre or a post segment. If we have one of those then
525 # the normal sorting rules will handle this case correctly.
526 if pre
is None and post
is None and dev
is not None:
527 _pre
: PrePostDevType
= NegativeInfinity
528 # Versions without a pre-release (except as noted above) should sort after
535 # Versions without a post segment should sort before those with one.
537 _post
: PrePostDevType
= NegativeInfinity
542 # Versions without a development segment should sort after those with one.
544 _dev
: PrePostDevType
= Infinity
550 # Versions without a local segment should sort before those with one.
551 _local
: LocalType
= NegativeInfinity
553 # Versions with a local segment need that segment parsed to implement
554 # the sorting rules in PEP440.
555 # - Alpha numeric segments sort before numeric segments
556 # - Alpha numeric segments sort lexicographically
557 # - Numeric segments sort numerically
558 # - Shorter versions sort before longer versions when the prefixes
561 (i
, "") if isinstance(i
, int) else (NegativeInfinity
, i
) for i
in local
564 return epoch
, _release
, _pre
, _post
, _dev
, _local