1"""Vendoered from
2https://github.com/pypa/packaging/blob/main/packaging/version.py
3"""
4# Copyright (c) Donald Stufft and individual contributors.
5# All rights reserved.
6
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions are met:
9
10# 1. Redistributions of source code must retain the above copyright notice,
11# this list of conditions and the following disclaimer.
12
13# 2. Redistributions in binary form must reproduce the above copyright
14# notice, this list of conditions and the following disclaimer in the
15# documentation and/or other materials provided with the distribution.
16
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28import collections
29import itertools
30import re
31import warnings
32from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union
33
34from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
35
36__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
37
38InfiniteTypes = Union[InfinityType, NegativeInfinityType]
39PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
40SubLocalType = Union[InfiniteTypes, int, str]
41LocalType = Union[
42 NegativeInfinityType,
43 Tuple[
44 Union[
45 SubLocalType,
46 Tuple[SubLocalType, str],
47 Tuple[NegativeInfinityType, SubLocalType],
48 ],
49 ...,
50 ],
51]
52CmpKey = Tuple[
53 int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
54]
55LegacyCmpKey = Tuple[int, Tuple[str, ...]]
56VersionComparisonMethod = Callable[
57 [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
58]
59
60_Version = collections.namedtuple(
61 "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
62)
63
64
65def parse(version: str) -> Union["LegacyVersion", "Version"]:
66 """Parse the given version from a string to an appropriate class.
67
68 Parameters
69 ----------
70 version : str
71 Version in a string format, eg. "0.9.1" or "1.2.dev0".
72
73 Returns
74 -------
75 version : :class:`Version` object or a :class:`LegacyVersion` object
76 Returned class depends on the given version: if is a valid
77 PEP 440 version or a legacy version.
78 """
79 try:
80 return Version(version)
81 except InvalidVersion:
82 return LegacyVersion(version)
83
84
85class InvalidVersion(ValueError):
86 """
87 An invalid version was found, users should refer to PEP 440.
88 """
89
90
91class _BaseVersion:
92 _key: Union[CmpKey, LegacyCmpKey]
93
94 def __hash__(self) -> int:
95 return hash(self._key)
96
97 # Please keep the duplicated `isinstance` check
98 # in the six comparisons hereunder
99 # unless you find a way to avoid adding overhead function calls.
100 def __lt__(self, other: "_BaseVersion") -> bool:
101 if not isinstance(other, _BaseVersion):
102 return NotImplemented
103
104 return self._key < other._key
105
106 def __le__(self, other: "_BaseVersion") -> bool:
107 if not isinstance(other, _BaseVersion):
108 return NotImplemented
109
110 return self._key <= other._key
111
112 def __eq__(self, other: object) -> bool:
113 if not isinstance(other, _BaseVersion):
114 return NotImplemented
115
116 return self._key == other._key
117
118 def __ge__(self, other: "_BaseVersion") -> bool:
119 if not isinstance(other, _BaseVersion):
120 return NotImplemented
121
122 return self._key >= other._key
123
124 def __gt__(self, other: "_BaseVersion") -> bool:
125 if not isinstance(other, _BaseVersion):
126 return NotImplemented
127
128 return self._key > other._key
129
130 def __ne__(self, other: object) -> bool:
131 if not isinstance(other, _BaseVersion):
132 return NotImplemented
133
134 return self._key != other._key
135
136
137class LegacyVersion(_BaseVersion):
138 def __init__(self, version: str) -> None:
139 self._version = str(version)
140 self._key = _legacy_cmpkey(self._version)
141
142 warnings.warn(
143 "Creating a LegacyVersion has been deprecated and will be "
144 "removed in the next major release",
145 DeprecationWarning,
146 )
147
148 def __str__(self) -> str:
149 return self._version
150
151 def __repr__(self) -> str:
152 return f"<LegacyVersion('{self}')>"
153
154 @property
155 def public(self) -> str:
156 return self._version
157
158 @property
159 def base_version(self) -> str:
160 return self._version
161
162 @property
163 def epoch(self) -> int:
164 return -1
165
166 @property
167 def release(self) -> None:
168 return None
169
170 @property
171 def pre(self) -> None:
172 return None
173
174 @property
175 def post(self) -> None:
176 return None
177
178 @property
179 def dev(self) -> None:
180 return None
181
182 @property
183 def local(self) -> None:
184 return None
185
186 @property
187 def is_prerelease(self) -> bool:
188 return False
189
190 @property
191 def is_postrelease(self) -> bool:
192 return False
193
194 @property
195 def is_devrelease(self) -> bool:
196 return False
197
198
199_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
200
201_legacy_version_replacement_map = {
202 "pre": "c",
203 "preview": "c",
204 "-": "final-",
205 "rc": "c",
206 "dev": "@",
207}
208
209
210def _parse_version_parts(s: str) -> Iterator[str]:
211 for part in _legacy_version_component_re.split(s):
212 part = _legacy_version_replacement_map.get(part, part)
213
214 if not part or part == ".":
215 continue
216
217 if part[:1] in "0123456789":
218 # pad for numeric comparison
219 yield part.zfill(8)
220 else:
221 yield "*" + part
222
223 # ensure that alpha/beta/candidate are before final
224 yield "*final"
225
226
227def _legacy_cmpkey(version: str) -> LegacyCmpKey:
228
229 # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
230 # greater than or equal to 0. This will effectively put the LegacyVersion,
231 # which uses the defacto standard originally implemented by setuptools,
232 # as before all PEP 440 versions.
233 epoch = -1
234
235 # This scheme is taken from pkg_resources.parse_version setuptools prior to
236 # it's adoption of the packaging library.
237 parts: List[str] = []
238 for part in _parse_version_parts(version.lower()):
239 if part.startswith("*"):
240 # remove "-" before a prerelease tag
241 if part < "*final":
242 while parts and parts[-1] == "*final-":
243 parts.pop()
244
245 # remove trailing zeros from each series of numeric parts
246 while parts and parts[-1] == "00000000":
247 parts.pop()
248
249 parts.append(part)
250
251 return epoch, tuple(parts)
252
253
254# Deliberately not anchored to the start and end of the string, to make it
255# easier for 3rd party code to reuse
256VERSION_PATTERN = r"""
257 v?
258 (?:
259 (?:(?P<epoch>[0-9]+)!)? # epoch
260 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
261 (?P<pre> # pre-release
262 [-_\.]?
263 (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
264 [-_\.]?
265 (?P<pre_n>[0-9]+)?
266 )?
267 (?P<post> # post release
268 (?:-(?P<post_n1>[0-9]+))
269 |
270 (?:
271 [-_\.]?
272 (?P<post_l>post|rev|r)
273 [-_\.]?
274 (?P<post_n2>[0-9]+)?
275 )
276 )?
277 (?P<dev> # dev release
278 [-_\.]?
279 (?P<dev_l>dev)
280 [-_\.]?
281 (?P<dev_n>[0-9]+)?
282 )?
283 )
284 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
285"""
286
287
288class Version(_BaseVersion):
289
290 _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
291
292 def __init__(self, version: str) -> None:
293
294 # Validate the version and parse it into pieces
295 match = self._regex.search(version)
296 if not match:
297 raise InvalidVersion(f"Invalid version: '{version}'")
298
299 # Store the parsed out pieces of the version
300 self._version = _Version(
301 epoch=int(match.group("epoch")) if match.group("epoch") else 0,
302 release=tuple(int(i) for i in match.group("release").split(".")),
303 pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
304 post=_parse_letter_version(
305 match.group("post_l"), match.group("post_n1") or match.group("post_n2")
306 ),
307 dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
308 local=_parse_local_version(match.group("local")),
309 )
310
311 # Generate a key which will be used for sorting
312 self._key = _cmpkey(
313 self._version.epoch,
314 self._version.release,
315 self._version.pre,
316 self._version.post,
317 self._version.dev,
318 self._version.local,
319 )
320
321 def __repr__(self) -> str:
322 return f"<Version('{self}')>"
323
324 def __str__(self) -> str:
325 parts = []
326
327 # Epoch
328 if self.epoch != 0:
329 parts.append(f"{self.epoch}!")
330
331 # Release segment
332 parts.append(".".join(str(x) for x in self.release))
333
334 # Pre-release
335 if self.pre is not None:
336 parts.append("".join(str(x) for x in self.pre))
337
338 # Post-release
339 if self.post is not None:
340 parts.append(f".post{self.post}")
341
342 # Development release
343 if self.dev is not None:
344 parts.append(f".dev{self.dev}")
345
346 # Local version segment
347 if self.local is not None:
348 parts.append(f"+{self.local}")
349
350 return "".join(parts)
351
352 @property
353 def epoch(self) -> int:
354 _epoch: int = self._version.epoch
355 return _epoch
356
357 @property
358 def release(self) -> Tuple[int, ...]:
359 _release: Tuple[int, ...] = self._version.release
360 return _release
361
362 @property
363 def pre(self) -> Optional[Tuple[str, int]]:
364 _pre: Optional[Tuple[str, int]] = self._version.pre
365 return _pre
366
367 @property
368 def post(self) -> Optional[int]:
369 return self._version.post[1] if self._version.post else None
370
371 @property
372 def dev(self) -> Optional[int]:
373 return self._version.dev[1] if self._version.dev else None
374
375 @property
376 def local(self) -> Optional[str]:
377 if self._version.local:
378 return ".".join(str(x) for x in self._version.local)
379 else:
380 return None
381
382 @property
383 def public(self) -> str:
384 return str(self).split("+", 1)[0]
385
386 @property
387 def base_version(self) -> str:
388 parts = []
389
390 # Epoch
391 if self.epoch != 0:
392 parts.append(f"{self.epoch}!")
393
394 # Release segment
395 parts.append(".".join(str(x) for x in self.release))
396
397 return "".join(parts)
398
399 @property
400 def is_prerelease(self) -> bool:
401 return self.dev is not None or self.pre is not None
402
403 @property
404 def is_postrelease(self) -> bool:
405 return self.post is not None
406
407 @property
408 def is_devrelease(self) -> bool:
409 return self.dev is not None
410
411 @property
412 def major(self) -> int:
413 return self.release[0] if len(self.release) >= 1 else 0
414
415 @property
416 def minor(self) -> int:
417 return self.release[1] if len(self.release) >= 2 else 0
418
419 @property
420 def micro(self) -> int:
421 return self.release[2] if len(self.release) >= 3 else 0
422
423
424def _parse_letter_version(
425 letter: str, number: Union[str, bytes, SupportsInt]
426) -> Optional[Tuple[str, int]]:
427
428 if letter:
429 # We consider there to be an implicit 0 in a pre-release if there is
430 # not a numeral associated with it.
431 if number is None:
432 number = 0
433
434 # We normalize any letters to their lower case form
435 letter = letter.lower()
436
437 # We consider some words to be alternate spellings of other words and
438 # in those cases we want to normalize the spellings to our preferred
439 # spelling.
440 if letter == "alpha":
441 letter = "a"
442 elif letter == "beta":
443 letter = "b"
444 elif letter in ["c", "pre", "preview"]:
445 letter = "rc"
446 elif letter in ["rev", "r"]:
447 letter = "post"
448
449 return letter, int(number)
450 if not letter and number:
451 # We assume if we are given a number, but we are not given a letter
452 # then this is using the implicit post release syntax (e.g. 1.0-1)
453 letter = "post"
454
455 return letter, int(number)
456
457 return None
458
459
460_local_version_separators = re.compile(r"[\._-]")
461
462
463def _parse_local_version(local: str) -> Optional[LocalType]:
464 """
465 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
466 """
467 if local is not None:
468 return tuple(
469 part.lower() if not part.isdigit() else int(part)
470 for part in _local_version_separators.split(local)
471 )
472 return None
473
474
475def _cmpkey(
476 epoch: int,
477 release: Tuple[int, ...],
478 pre: Optional[Tuple[str, int]],
479 post: Optional[Tuple[str, int]],
480 dev: Optional[Tuple[str, int]],
481 local: Optional[Tuple[SubLocalType]],
482) -> CmpKey:
483
484 # When we compare a release version, we want to compare it with all of the
485 # trailing zeros removed. So we'll use a reverse the list, drop all the now
486 # leading zeros until we come to something non zero, then take the rest
487 # re-reverse it back into the correct order and make it a tuple and use
488 # that for our sorting key.
489 _release = tuple(
490 reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
491 )
492
493 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
494 # We'll do this by abusing the pre segment, but we _only_ want to do this
495 # if there is not a pre or a post segment. If we have one of those then
496 # the normal sorting rules will handle this case correctly.
497 if pre is None and post is None and dev is not None:
498 _pre: PrePostDevType = NegativeInfinity
499 # Versions without a pre-release (except as noted above) should sort after
500 # those with one.
501 elif pre is None:
502 _pre = Infinity
503 else:
504 _pre = pre
505
506 # Versions without a post segment should sort before those with one.
507 if post is None:
508 _post: PrePostDevType = NegativeInfinity
509
510 else:
511 _post = post
512
513 # Versions without a development segment should sort after those with one.
514 if dev is None:
515 _dev: PrePostDevType = Infinity
516
517 else:
518 _dev = dev
519
520 if local is None:
521 # Versions without a local segment should sort before those with one.
522 _local: LocalType = NegativeInfinity
523 else:
524 # Versions with a local segment need that segment parsed to implement
525 # the sorting rules in PEP440.
526 # - Alpha numeric segments sort before numeric segments
527 # - Alpha numeric segments sort lexicographically
528 # - Numeric segments sort numerically
529 # - Shorter versions sort before longer versions when the prefixes
530 # match exactly
531 _local = tuple(
532 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
533 )
534
535 return epoch, _release, _pre, _post, _dev, _local