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