1# Vendored from https://github.com/pypa/packaging/blob/main/src/packaging/_structures.py
2# and https://github.com/pypa/packaging/blob/main/src/packaging/version.py
3# changeset 24e5350b2ff3c5c7a36676c2af5f2cb39fd1baf8
4
5# This file is dual licensed under the terms of the Apache License, Version
6# 2.0, and the BSD License. Licence at LICENSES/PACKAGING_LICENSE
7from __future__ import annotations
8
9from collections.abc import Callable
10import itertools
11import re
12from typing import (
13 Any,
14 NamedTuple,
15 SupportsInt,
16 TypeAlias,
17)
18
19__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"]
20
21
22class InfinityType:
23 def __repr__(self) -> str:
24 return "Infinity"
25
26 def __hash__(self) -> int:
27 return hash(repr(self))
28
29 def __lt__(self, other: object) -> bool:
30 return False
31
32 def __le__(self, other: object) -> bool:
33 return False
34
35 def __eq__(self, other: object) -> bool:
36 return isinstance(other, type(self))
37
38 def __gt__(self, other: object) -> bool:
39 return True
40
41 def __ge__(self, other: object) -> bool:
42 return True
43
44 def __neg__(self: object) -> NegativeInfinityType:
45 return NegativeInfinity
46
47
48Infinity = InfinityType()
49
50
51class NegativeInfinityType:
52 def __repr__(self) -> str:
53 return "-Infinity"
54
55 def __hash__(self) -> int:
56 return hash(repr(self))
57
58 def __lt__(self, other: object) -> bool:
59 return True
60
61 def __le__(self, other: object) -> bool:
62 return True
63
64 def __eq__(self, other: object) -> bool:
65 return isinstance(other, type(self))
66
67 def __gt__(self, other: object) -> bool:
68 return False
69
70 def __ge__(self, other: object) -> bool:
71 return False
72
73 def __neg__(self: object) -> InfinityType:
74 return Infinity
75
76
77NegativeInfinity = NegativeInfinityType()
78
79
80LocalType: TypeAlias = tuple[int | str, ...]
81
82CmpPrePostDevType: TypeAlias = InfinityType | NegativeInfinityType | tuple[str, int]
83CmpLocalType: TypeAlias = (
84 NegativeInfinityType
85 | tuple[tuple[int, str] | tuple[NegativeInfinityType, int | str], ...]
86)
87CmpKey: TypeAlias = tuple[
88 int,
89 tuple[int, ...],
90 CmpPrePostDevType,
91 CmpPrePostDevType,
92 CmpPrePostDevType,
93 CmpLocalType,
94]
95VersionComparisonMethod: TypeAlias = Callable[[CmpKey, CmpKey], bool]
96
97
98class _Version(NamedTuple):
99 epoch: int
100 release: tuple[int, ...]
101 dev: tuple[str, int] | None
102 pre: tuple[str, int] | None
103 post: tuple[str, int] | None
104 local: LocalType | None
105
106
107def parse(version: str) -> Version:
108 return Version(version)
109
110
111# The docstring is from an older version of the packaging library to avoid
112# errors in the docstring validation.
113class InvalidVersion(ValueError):
114 """
115 An invalid version was found, users should refer to PEP 440.
116
117 The ``InvalidVersion`` exception is raised when a version string is
118 improperly formatted. Pandas uses this exception to ensure that all
119 version strings are PEP 440 compliant.
120
121 See Also
122 --------
123 util.version.Version : Class for handling and parsing version strings.
124
125 Examples
126 --------
127 >>> pd.util.version.Version("1.")
128 Traceback (most recent call last):
129 InvalidVersion: Invalid version: '1.'
130 """
131
132 __module__ = "pandas.errors"
133
134
135class _BaseVersion:
136 _key: tuple[Any, ...]
137
138 def __hash__(self) -> int:
139 return hash(self._key)
140
141 # Please keep the duplicated `isinstance` check
142 # in the six comparisons hereunder
143 # unless you find a way to avoid adding overhead function calls.
144 def __lt__(self, other: _BaseVersion) -> bool:
145 if not isinstance(other, _BaseVersion):
146 return NotImplemented
147
148 return self._key < other._key
149
150 def __le__(self, other: _BaseVersion) -> bool:
151 if not isinstance(other, _BaseVersion):
152 return NotImplemented
153
154 return self._key <= other._key
155
156 def __eq__(self, other: object) -> bool:
157 if not isinstance(other, _BaseVersion):
158 return NotImplemented
159
160 return self._key == other._key
161
162 def __ge__(self, other: _BaseVersion) -> bool:
163 if not isinstance(other, _BaseVersion):
164 return NotImplemented
165
166 return self._key >= other._key
167
168 def __gt__(self, other: _BaseVersion) -> bool:
169 if not isinstance(other, _BaseVersion):
170 return NotImplemented
171
172 return self._key > other._key
173
174 def __ne__(self, other: object) -> bool:
175 if not isinstance(other, _BaseVersion):
176 return NotImplemented
177
178 return self._key != other._key
179
180
181# Deliberately not anchored to the start and end of the string, to make it
182# easier for 3rd party code to reuse
183_VERSION_PATTERN = r"""
184 v?
185 (?:
186 (?:(?P<epoch>[0-9]+)!)? # epoch
187 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
188 (?P<pre> # pre-release
189 [-_\.]?
190 (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
191 [-_\.]?
192 (?P<pre_n>[0-9]+)?
193 )?
194 (?P<post> # post release
195 (?:-(?P<post_n1>[0-9]+))
196 |
197 (?:
198 [-_\.]?
199 (?P<post_l>post|rev|r)
200 [-_\.]?
201 (?P<post_n2>[0-9]+)?
202 )
203 )?
204 (?P<dev> # dev release
205 [-_\.]?
206 (?P<dev_l>dev)
207 [-_\.]?
208 (?P<dev_n>[0-9]+)?
209 )?
210 )
211 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
212"""
213
214VERSION_PATTERN = _VERSION_PATTERN
215
216
217class Version(_BaseVersion):
218 _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
219 _key: CmpKey
220
221 def __init__(self, version: str) -> None:
222 # Validate the version and parse it into pieces
223 match = self._regex.search(version)
224 if not match:
225 raise InvalidVersion(f"Invalid version: '{version}'")
226
227 # Store the parsed out pieces of the version
228 self._version = _Version(
229 epoch=int(match.group("epoch")) if match.group("epoch") else 0,
230 release=tuple(int(i) for i in match.group("release").split(".")),
231 pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
232 post=_parse_letter_version(
233 match.group("post_l"), match.group("post_n1") or match.group("post_n2")
234 ),
235 dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
236 local=_parse_local_version(match.group("local")),
237 )
238
239 # Generate a key which will be used for sorting
240 self._key = _cmpkey(
241 self._version.epoch,
242 self._version.release,
243 self._version.pre,
244 self._version.post,
245 self._version.dev,
246 self._version.local,
247 )
248
249 def __repr__(self) -> str:
250 return f"<Version('{self}')>"
251
252 def __str__(self) -> str:
253 parts = []
254
255 # Epoch
256 if self.epoch != 0:
257 parts.append(f"{self.epoch}!")
258
259 # Release segment
260 parts.append(".".join(str(x) for x in self.release))
261
262 # Pre-release
263 if self.pre is not None:
264 parts.append("".join(str(x) for x in self.pre))
265
266 # Post-release
267 if self.post is not None:
268 parts.append(f".post{self.post}")
269
270 # Development release
271 if self.dev is not None:
272 parts.append(f".dev{self.dev}")
273
274 # Local version segment
275 if self.local is not None:
276 parts.append(f"+{self.local}")
277
278 return "".join(parts)
279
280 @property
281 def epoch(self) -> int:
282 return self._version.epoch
283
284 @property
285 def release(self) -> tuple[int, ...]:
286 return self._version.release
287
288 @property
289 def pre(self) -> tuple[str, int] | None:
290 return self._version.pre
291
292 @property
293 def post(self) -> int | None:
294 return self._version.post[1] if self._version.post else None
295
296 @property
297 def dev(self) -> int | None:
298 return self._version.dev[1] if self._version.dev else None
299
300 @property
301 def local(self) -> str | None:
302 if self._version.local:
303 return ".".join(str(x) for x in self._version.local)
304 else:
305 return None
306
307 @property
308 def public(self) -> str:
309 return str(self).split("+", 1)[0]
310
311 @property
312 def base_version(self) -> str:
313 parts = []
314
315 # Epoch
316 if self.epoch != 0:
317 parts.append(f"{self.epoch}!")
318
319 # Release segment
320 parts.append(".".join(str(x) for x in self.release))
321
322 return "".join(parts)
323
324 @property
325 def is_prerelease(self) -> bool:
326 return self.dev is not None or self.pre is not None
327
328 @property
329 def is_postrelease(self) -> bool:
330 return self.post is not None
331
332 @property
333 def is_devrelease(self) -> bool:
334 return self.dev is not None
335
336 @property
337 def major(self) -> int:
338 return self.release[0] if len(self.release) >= 1 else 0
339
340 @property
341 def minor(self) -> int:
342 return self.release[1] if len(self.release) >= 2 else 0
343
344 @property
345 def micro(self) -> int:
346 return self.release[2] if len(self.release) >= 3 else 0
347
348
349def _parse_letter_version(
350 letter: str | None, number: str | bytes | SupportsInt | None
351) -> tuple[str, int] | None:
352 if letter:
353 # We consider there to be an implicit 0 in a pre-release if there is
354 # not a numeral associated with it.
355 if number is None:
356 number = 0
357
358 # We normalize any letters to their lower case form
359 letter = letter.lower()
360
361 # We consider some words to be alternate spellings of other words and
362 # in those cases we want to normalize the spellings to our preferred
363 # spelling.
364 if letter == "alpha":
365 letter = "a"
366 elif letter == "beta":
367 letter = "b"
368 elif letter in ["c", "pre", "preview"]:
369 letter = "rc"
370 elif letter in ["rev", "r"]:
371 letter = "post"
372
373 return letter, int(number)
374 if not letter and number:
375 # We assume if we are given a number, but we are not given a letter
376 # then this is using the implicit post release syntax (e.g. 1.0-1)
377 letter = "post"
378
379 return letter, int(number)
380
381 return None
382
383
384_local_version_separators = re.compile(r"[\._-]")
385
386
387def _parse_local_version(local: str | None) -> LocalType | None:
388 if local is not None:
389 return tuple(
390 part.lower() if not part.isdigit() else int(part)
391 for part in _local_version_separators.split(local)
392 )
393 return None
394
395
396def _cmpkey(
397 epoch: int,
398 release: tuple[int, ...],
399 pre: tuple[str, int] | None,
400 post: tuple[str, int] | None,
401 dev: tuple[str, int] | None,
402 local: LocalType | None,
403) -> CmpKey:
404 # When we compare a release version, we want to compare it with all of the
405 # trailing zeros removed. So we'll use a reverse the list, drop all the now
406 # leading zeros until we come to something non zero, then take the rest
407 # re-reverse it back into the correct order and make it a tuple and use
408 # that for our sorting key.
409 _release = tuple(
410 reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
411 )
412
413 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
414 # We'll do this by abusing the pre segment, but we _only_ want to do this
415 # if there is not a pre or a post segment. If we have one of those then
416 # the normal sorting rules will handle this case correctly.
417 if pre is None and post is None and dev is not None:
418 _pre: CmpPrePostDevType = NegativeInfinity
419 # Versions without a pre-release (except as noted above) should sort after
420 # those with one.
421 elif pre is None:
422 _pre = Infinity
423 else:
424 _pre = pre
425
426 # Versions without a post segment should sort before those with one.
427 if post is None:
428 _post: CmpPrePostDevType = NegativeInfinity
429
430 else:
431 _post = post
432
433 # Versions without a development segment should sort after those with one.
434 if dev is None:
435 _dev: CmpPrePostDevType = Infinity
436
437 else:
438 _dev = dev
439
440 if local is None:
441 # Versions without a local segment should sort before those with one.
442 _local: CmpLocalType = NegativeInfinity
443 else:
444 # Versions with a local segment need that segment parsed to implement
445 # the sorting rules in PEP440.
446 # - Alpha numeric segments sort before numeric segments
447 # - Alpha numeric segments sort lexicographically
448 # - Numeric segments sort numerically
449 # - Shorter versions sort before longer versions when the prefixes
450 # match exactly
451 _local = tuple(
452 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
453 )
454
455 return epoch, _release, _pre, _post, _dev, _local