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
12 from typing
import Any
, Callable
, NamedTuple
, Optional
, SupportsInt
, Tuple
, Union
14 from ._structures
import Infinity
, InfinityType
, NegativeInfinity
, NegativeInfinityType
16 __all__
= ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"]
18 LocalType
= Tuple
[Union
[int, str], ...]
20 CmpPrePostDevType
= Union
[InfinityType
, NegativeInfinityType
, Tuple
[str, int]]
23 Tuple
[Union
[Tuple
[int, str], Tuple
[NegativeInfinityType
, Union
[int, str]]], ...],
33 VersionComparisonMethod
= Callable
[[CmpKey
, CmpKey
], bool]
36 class _Version(NamedTuple
):
38 release
: Tuple
[int, ...]
39 dev
: Optional
[Tuple
[str, int]]
40 pre
: Optional
[Tuple
[str, int]]
41 post
: Optional
[Tuple
[str, int]]
42 local
: Optional
[LocalType
]
45 def parse(version
: str) -> "Version":
46 """Parse the given version string.
51 :param version: The version string to parse.
52 :raises InvalidVersion: When the version string is not a valid version.
54 return Version(version
)
57 class InvalidVersion(ValueError):
58 """Raised when a version string is not a valid version.
60 >>> Version("invalid")
61 Traceback (most recent call last):
63 packaging.version.InvalidVersion: Invalid version: 'invalid'
70 def __hash__(self
) -> int:
71 return hash(self
._key
)
73 # Please keep the duplicated `isinstance` check
74 # in the six comparisons hereunder
75 # unless you find a way to avoid adding overhead function calls.
76 def __lt__(self
, other
: "_BaseVersion") -> bool:
77 if not isinstance(other
, _BaseVersion
):
80 return self
._key
< other
._key
82 def __le__(self
, other
: "_BaseVersion") -> bool:
83 if not isinstance(other
, _BaseVersion
):
86 return self
._key
<= other
._key
88 def __eq__(self
, other
: object) -> bool:
89 if not isinstance(other
, _BaseVersion
):
92 return self
._key
== other
._key
94 def __ge__(self
, other
: "_BaseVersion") -> bool:
95 if not isinstance(other
, _BaseVersion
):
98 return self
._key
>= other
._key
100 def __gt__(self
, other
: "_BaseVersion") -> bool:
101 if not isinstance(other
, _BaseVersion
):
102 return NotImplemented
104 return self
._key
> other
._key
106 def __ne__(self
, other
: object) -> bool:
107 if not isinstance(other
, _BaseVersion
):
108 return NotImplemented
110 return self
._key
!= other
._key
113 # Deliberately not anchored to the start and end of the string, to make it
114 # easier for 3rd party code to reuse
115 _VERSION_PATTERN
= r
"""
118 (?:(?P<epoch>[0-9]+)!)? # epoch
119 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
120 (?P<pre> # pre-release
122 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
126 (?P<post> # post release
127 (?:-(?P<post_n1>[0-9]+))
131 (?P<post_l>post|rev|r)
136 (?P<dev> # dev release
143 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
146 VERSION_PATTERN
= _VERSION_PATTERN
148 A string containing the regular expression used to match a valid version.
150 The pattern is not anchored at either end, and is intended for embedding in larger
151 expressions (for example, matching a version number as part of a file name). The
152 regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
159 class Version(_BaseVersion
):
160 """This class abstracts handling of a project's versions.
162 A :class:`Version` instance is comparison aware and can be compared and
163 sorted using the standard Python interfaces.
165 >>> v1 = Version("1.0a5")
166 >>> v2 = Version("1.0")
183 _regex
= re
.compile(r
"^\s*" + VERSION_PATTERN
+ r
"\s*$", re
.VERBOSE | re
.IGNORECASE
)
186 def __init__(self
, version
: str) -> None:
187 """Initialize a Version object.
190 The string representation of a version which will be parsed and normalized
192 :raises InvalidVersion:
193 If the ``version`` does not conform to PEP 440 in any way then this
194 exception will be raised.
197 # Validate the version and parse it into pieces
198 match
= self
._regex
.search(version
)
200 raise InvalidVersion(f
"Invalid version: '{version}'")
202 # Store the parsed out pieces of the version
203 self
._version
= _Version(
204 epoch
=int(match
.group("epoch")) if match
.group("epoch") else 0,
205 release
=tuple(int(i
) for i
in match
.group("release").split(".")),
206 pre
=_parse_letter_version(match
.group("pre_l"), match
.group("pre_n")),
207 post
=_parse_letter_version(
208 match
.group("post_l"), match
.group("post_n1") or match
.group("post_n2")
210 dev
=_parse_letter_version(match
.group("dev_l"), match
.group("dev_n")),
211 local
=_parse_local_version(match
.group("local")),
214 # Generate a key which will be used for sorting
217 self
._version
.release
,
224 def __repr__(self
) -> str:
225 """A representation of the Version that shows all internal state.
230 return f
"<Version('{self}')>"
232 def __str__(self
) -> str:
233 """A string representation of the version that can be rounded-tripped.
235 >>> str(Version("1.0a5"))
242 parts
.append(f
"{self.epoch}!")
245 parts
.append(".".join(str(x
) for x
in self
.release
))
248 if self
.pre
is not None:
249 parts
.append("".join(str(x
) for x
in self
.pre
))
252 if self
.post
is not None:
253 parts
.append(f
".post{self.post}")
255 # Development release
256 if self
.dev
is not None:
257 parts
.append(f
".dev{self.dev}")
259 # Local version segment
260 if self
.local
is not None:
261 parts
.append(f
"+{self.local}")
263 return "".join(parts
)
266 def epoch(self
) -> int:
267 """The epoch of the version.
269 >>> Version("2.0.0").epoch
271 >>> Version("1!2.0.0").epoch
274 return self
._version
.epoch
277 def release(self
) -> Tuple
[int, ...]:
278 """The components of the "release" segment of the version.
280 >>> Version("1.2.3").release
282 >>> Version("2.0.0").release
284 >>> Version("1!2.0.0.post0").release
287 Includes trailing zeroes but not the epoch or any pre-release / development /
288 post-release suffixes.
290 return 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 return self
._version
.pre
308 def post(self
) -> Optional
[int]:
309 """The post-release number of the version.
311 >>> print(Version("1.2.3").post)
313 >>> Version("1.2.3.post1").post
316 return self
._version
.post
[1] if self
._version
.post
else None
319 def dev(self
) -> Optional
[int]:
320 """The development number of the version.
322 >>> print(Version("1.2.3").dev)
324 >>> Version("1.2.3.dev1").dev
327 return self
._version
.dev
[1] if self
._version
.dev
else None
330 def local(self
) -> Optional
[str]:
331 """The local version segment of the version.
333 >>> print(Version("1.2.3").local)
335 >>> Version("1.2.3+abc").local
338 if self
._version
.local
:
339 return ".".join(str(x
) for x
in self
._version
.local
)
344 def public(self
) -> str:
345 """The public portion of the version.
347 >>> Version("1.2.3").public
349 >>> Version("1.2.3+abc").public
351 >>> Version("1.2.3+abc.dev1").public
354 return str(self
).split("+", 1)[0]
357 def base_version(self
) -> str:
358 """The "base version" of the version.
360 >>> Version("1.2.3").base_version
362 >>> Version("1.2.3+abc").base_version
364 >>> Version("1!1.2.3+abc.dev1").base_version
367 The "base version" is the public version of the project without any pre or post
374 parts
.append(f
"{self.epoch}!")
377 parts
.append(".".join(str(x
) for x
in self
.release
))
379 return "".join(parts
)
382 def is_prerelease(self
) -> bool:
383 """Whether this version is a pre-release.
385 >>> Version("1.2.3").is_prerelease
387 >>> Version("1.2.3a1").is_prerelease
389 >>> Version("1.2.3b1").is_prerelease
391 >>> Version("1.2.3rc1").is_prerelease
393 >>> Version("1.2.3dev1").is_prerelease
396 return self
.dev
is not None or self
.pre
is not None
399 def is_postrelease(self
) -> bool:
400 """Whether this version is a post-release.
402 >>> Version("1.2.3").is_postrelease
404 >>> Version("1.2.3.post1").is_postrelease
407 return self
.post
is not None
410 def is_devrelease(self
) -> bool:
411 """Whether this version is a development release.
413 >>> Version("1.2.3").is_devrelease
415 >>> Version("1.2.3.dev1").is_devrelease
418 return self
.dev
is not None
421 def major(self
) -> int:
422 """The first item of :attr:`release` or ``0`` if unavailable.
424 >>> Version("1.2.3").major
427 return self
.release
[0] if len(self
.release
) >= 1 else 0
430 def minor(self
) -> int:
431 """The second item of :attr:`release` or ``0`` if unavailable.
433 >>> Version("1.2.3").minor
435 >>> Version("1").minor
438 return self
.release
[1] if len(self
.release
) >= 2 else 0
441 def micro(self
) -> int:
442 """The third item of :attr:`release` or ``0`` if unavailable.
444 >>> Version("1.2.3").micro
446 >>> Version("1").micro
449 return self
.release
[2] if len(self
.release
) >= 3 else 0
452 def _parse_letter_version(
453 letter
: Optional
[str], number
: Union
[str, bytes
, SupportsInt
, None]
454 ) -> Optional
[Tuple
[str, int]]:
457 # We consider there to be an implicit 0 in a pre-release if there is
458 # not a numeral associated with it.
462 # We normalize any letters to their lower case form
463 letter
= letter
.lower()
465 # We consider some words to be alternate spellings of other words and
466 # in those cases we want to normalize the spellings to our preferred
468 if letter
== "alpha":
470 elif letter
== "beta":
472 elif letter
in ["c", "pre", "preview"]:
474 elif letter
in ["rev", "r"]:
477 return letter
, int(number
)
478 if not letter
and number
:
479 # We assume if we are given a number, but we are not given a letter
480 # then this is using the implicit post release syntax (e.g. 1.0-1)
483 return letter
, int(number
)
488 _local_version_separators
= re
.compile(r
"[\._-]")
491 def _parse_local_version(local
: Optional
[str]) -> Optional
[LocalType
]:
493 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
495 if local
is not None:
497 part
.lower() if not part
.isdigit() else int(part
)
498 for part
in _local_version_separators
.split(local
)
505 release
: Tuple
[int, ...],
506 pre
: Optional
[Tuple
[str, int]],
507 post
: Optional
[Tuple
[str, int]],
508 dev
: Optional
[Tuple
[str, int]],
509 local
: Optional
[LocalType
],
512 # When we compare a release version, we want to compare it with all of the
513 # trailing zeros removed. So we'll use a reverse the list, drop all the now
514 # leading zeros until we come to something non zero, then take the rest
515 # re-reverse it back into the correct order and make it a tuple and use
516 # that for our sorting key.
518 reversed(list(itertools
.dropwhile(lambda x
: x
== 0, reversed(release
))))
521 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
522 # We'll do this by abusing the pre segment, but we _only_ want to do this
523 # if there is not a pre or a post segment. If we have one of those then
524 # the normal sorting rules will handle this case correctly.
525 if pre
is None and post
is None and dev
is not None:
526 _pre
: CmpPrePostDevType
= NegativeInfinity
527 # Versions without a pre-release (except as noted above) should sort after
534 # Versions without a post segment should sort before those with one.
536 _post
: CmpPrePostDevType
= NegativeInfinity
541 # Versions without a development segment should sort after those with one.
543 _dev
: CmpPrePostDevType
= Infinity
549 # Versions without a local segment should sort before those with one.
550 _local
: CmpLocalType
= NegativeInfinity
552 # Versions with a local segment need that segment parsed to implement
553 # the sorting rules in PEP440.
554 # - Alpha numeric segments sort before numeric segments
555 # - Alpha numeric segments sort lexicographically
556 # - Numeric segments sort numerically
557 # - Shorter versions sort before longer versions when the prefixes
560 (i
, "") if isinstance(i
, int) else (NegativeInfinity
, i
) for i
in local
563 return epoch
, _release
, _pre
, _post
, _dev
, _local