]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
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. | |
4 | """ | |
5 | .. testsetup:: | |
6 | ||
7 | from packaging.version import parse, Version | |
8 | """ | |
9 | ||
10 | import itertools | |
11 | import re | |
12 | from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union | |
13 | ||
14 | from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType | |
15 | ||
16 | __all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] | |
17 | ||
18 | LocalType = Tuple[Union[int, str], ...] | |
19 | ||
20 | CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] | |
21 | CmpLocalType = Union[ | |
22 | NegativeInfinityType, | |
23 | Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], | |
24 | ] | |
25 | CmpKey = Tuple[ | |
26 | int, | |
27 | Tuple[int, ...], | |
28 | CmpPrePostDevType, | |
29 | CmpPrePostDevType, | |
30 | CmpPrePostDevType, | |
31 | CmpLocalType, | |
32 | ] | |
33 | VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] | |
34 | ||
35 | ||
36 | class _Version(NamedTuple): | |
37 | epoch: int | |
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] | |
43 | ||
44 | ||
45 | def parse(version: str) -> "Version": | |
46 | """Parse the given version string. | |
47 | ||
48 | >>> parse('1.0.dev1') | |
49 | <Version('1.0.dev1')> | |
50 | ||
51 | :param version: The version string to parse. | |
52 | :raises InvalidVersion: When the version string is not a valid version. | |
53 | """ | |
54 | return Version(version) | |
55 | ||
56 | ||
57 | class InvalidVersion(ValueError): | |
58 | """Raised when a version string is not a valid version. | |
59 | ||
60 | >>> Version("invalid") | |
61 | Traceback (most recent call last): | |
62 | ... | |
63 | packaging.version.InvalidVersion: Invalid version: 'invalid' | |
64 | """ | |
65 | ||
66 | ||
67 | class _BaseVersion: | |
68 | _key: Tuple[Any, ...] | |
69 | ||
70 | def __hash__(self) -> int: | |
71 | return hash(self._key) | |
72 | ||
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): | |
78 | return NotImplemented | |
79 | ||
80 | return self._key < other._key | |
81 | ||
82 | def __le__(self, other: "_BaseVersion") -> bool: | |
83 | if not isinstance(other, _BaseVersion): | |
84 | return NotImplemented | |
85 | ||
86 | return self._key <= other._key | |
87 | ||
88 | def __eq__(self, other: object) -> bool: | |
89 | if not isinstance(other, _BaseVersion): | |
90 | return NotImplemented | |
91 | ||
92 | return self._key == other._key | |
93 | ||
94 | def __ge__(self, other: "_BaseVersion") -> bool: | |
95 | if not isinstance(other, _BaseVersion): | |
96 | return NotImplemented | |
97 | ||
98 | return self._key >= other._key | |
99 | ||
100 | def __gt__(self, other: "_BaseVersion") -> bool: | |
101 | if not isinstance(other, _BaseVersion): | |
102 | return NotImplemented | |
103 | ||
104 | return self._key > other._key | |
105 | ||
106 | def __ne__(self, other: object) -> bool: | |
107 | if not isinstance(other, _BaseVersion): | |
108 | return NotImplemented | |
109 | ||
110 | return self._key != other._key | |
111 | ||
112 | ||
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""" | |
116 | v? | |
117 | (?: | |
118 | (?:(?P<epoch>[0-9]+)!)? # epoch | |
119 | (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment | |
120 | (?P<pre> # pre-release | |
121 | [-_\.]? | |
122 | (?P<pre_l>alpha|a|beta|b|preview|pre|c|rc) | |
123 | [-_\.]? | |
124 | (?P<pre_n>[0-9]+)? | |
125 | )? | |
126 | (?P<post> # post release | |
127 | (?:-(?P<post_n1>[0-9]+)) | |
128 | | | |
129 | (?: | |
130 | [-_\.]? | |
131 | (?P<post_l>post|rev|r) | |
132 | [-_\.]? | |
133 | (?P<post_n2>[0-9]+)? | |
134 | ) | |
135 | )? | |
136 | (?P<dev> # dev release | |
137 | [-_\.]? | |
138 | (?P<dev_l>dev) | |
139 | [-_\.]? | |
140 | (?P<dev_n>[0-9]+)? | |
141 | )? | |
142 | ) | |
143 | (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version | |
144 | """ | |
145 | ||
146 | VERSION_PATTERN = _VERSION_PATTERN | |
147 | """ | |
148 | A string containing the regular expression used to match a valid version. | |
149 | ||
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`` | |
153 | flags set. | |
154 | ||
155 | :meta hide-value: | |
156 | """ | |
157 | ||
158 | ||
159 | class Version(_BaseVersion): | |
160 | """This class abstracts handling of a project's versions. | |
161 | ||
162 | A :class:`Version` instance is comparison aware and can be compared and | |
163 | sorted using the standard Python interfaces. | |
164 | ||
165 | >>> v1 = Version("1.0a5") | |
166 | >>> v2 = Version("1.0") | |
167 | >>> v1 | |
168 | <Version('1.0a5')> | |
169 | >>> v2 | |
170 | <Version('1.0')> | |
171 | >>> v1 < v2 | |
172 | True | |
173 | >>> v1 == v2 | |
174 | False | |
175 | >>> v1 > v2 | |
176 | False | |
177 | >>> v1 >= v2 | |
178 | False | |
179 | >>> v1 <= v2 | |
180 | True | |
181 | """ | |
182 | ||
183 | _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) | |
184 | _key: CmpKey | |
185 | ||
186 | def __init__(self, version: str) -> None: | |
187 | """Initialize a Version object. | |
188 | ||
189 | :param version: | |
190 | The string representation of a version which will be parsed and normalized | |
191 | before use. | |
192 | :raises InvalidVersion: | |
193 | If the ``version`` does not conform to PEP 440 in any way then this | |
194 | exception will be raised. | |
195 | """ | |
196 | ||
197 | # Validate the version and parse it into pieces | |
198 | match = self._regex.search(version) | |
199 | if not match: | |
200 | raise InvalidVersion(f"Invalid version: '{version}'") | |
201 | ||
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") | |
209 | ), | |
210 | dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), | |
211 | local=_parse_local_version(match.group("local")), | |
212 | ) | |
213 | ||
214 | # Generate a key which will be used for sorting | |
215 | self._key = _cmpkey( | |
216 | self._version.epoch, | |
217 | self._version.release, | |
218 | self._version.pre, | |
219 | self._version.post, | |
220 | self._version.dev, | |
221 | self._version.local, | |
222 | ) | |
223 | ||
224 | def __repr__(self) -> str: | |
225 | """A representation of the Version that shows all internal state. | |
226 | ||
227 | >>> Version('1.0.0') | |
228 | <Version('1.0.0')> | |
229 | """ | |
230 | return f"<Version('{self}')>" | |
231 | ||
232 | def __str__(self) -> str: | |
233 | """A string representation of the version that can be rounded-tripped. | |
234 | ||
235 | >>> str(Version("1.0a5")) | |
236 | '1.0a5' | |
237 | """ | |
238 | parts = [] | |
239 | ||
240 | # Epoch | |
241 | if self.epoch != 0: | |
242 | parts.append(f"{self.epoch}!") | |
243 | ||
244 | # Release segment | |
245 | parts.append(".".join(str(x) for x in self.release)) | |
246 | ||
247 | # Pre-release | |
248 | if self.pre is not None: | |
249 | parts.append("".join(str(x) for x in self.pre)) | |
250 | ||
251 | # Post-release | |
252 | if self.post is not None: | |
253 | parts.append(f".post{self.post}") | |
254 | ||
255 | # Development release | |
256 | if self.dev is not None: | |
257 | parts.append(f".dev{self.dev}") | |
258 | ||
259 | # Local version segment | |
260 | if self.local is not None: | |
261 | parts.append(f"+{self.local}") | |
262 | ||
263 | return "".join(parts) | |
264 | ||
265 | @property | |
266 | def epoch(self) -> int: | |
267 | """The epoch of the version. | |
268 | ||
269 | >>> Version("2.0.0").epoch | |
270 | 0 | |
271 | >>> Version("1!2.0.0").epoch | |
272 | 1 | |
273 | """ | |
274 | return self._version.epoch | |
275 | ||
276 | @property | |
277 | def release(self) -> Tuple[int, ...]: | |
278 | """The components of the "release" segment of the version. | |
279 | ||
280 | >>> Version("1.2.3").release | |
281 | (1, 2, 3) | |
282 | >>> Version("2.0.0").release | |
283 | (2, 0, 0) | |
284 | >>> Version("1!2.0.0.post0").release | |
285 | (2, 0, 0) | |
286 | ||
287 | Includes trailing zeroes but not the epoch or any pre-release / development / | |
288 | post-release suffixes. | |
289 | """ | |
290 | return self._version.release | |
291 | ||
292 | @property | |
293 | def pre(self) -> Optional[Tuple[str, int]]: | |
294 | """The pre-release segment of the version. | |
295 | ||
296 | >>> print(Version("1.2.3").pre) | |
297 | None | |
298 | >>> Version("1.2.3a1").pre | |
299 | ('a', 1) | |
300 | >>> Version("1.2.3b1").pre | |
301 | ('b', 1) | |
302 | >>> Version("1.2.3rc1").pre | |
303 | ('rc', 1) | |
304 | """ | |
305 | return self._version.pre | |
306 | ||
307 | @property | |
308 | def post(self) -> Optional[int]: | |
309 | """The post-release number of the version. | |
310 | ||
311 | >>> print(Version("1.2.3").post) | |
312 | None | |
313 | >>> Version("1.2.3.post1").post | |
314 | 1 | |
315 | """ | |
316 | return self._version.post[1] if self._version.post else None | |
317 | ||
318 | @property | |
319 | def dev(self) -> Optional[int]: | |
320 | """The development number of the version. | |
321 | ||
322 | >>> print(Version("1.2.3").dev) | |
323 | None | |
324 | >>> Version("1.2.3.dev1").dev | |
325 | 1 | |
326 | """ | |
327 | return self._version.dev[1] if self._version.dev else None | |
328 | ||
329 | @property | |
330 | def local(self) -> Optional[str]: | |
331 | """The local version segment of the version. | |
332 | ||
333 | >>> print(Version("1.2.3").local) | |
334 | None | |
335 | >>> Version("1.2.3+abc").local | |
336 | 'abc' | |
337 | """ | |
338 | if self._version.local: | |
339 | return ".".join(str(x) for x in self._version.local) | |
340 | else: | |
341 | return None | |
342 | ||
343 | @property | |
344 | def public(self) -> str: | |
345 | """The public portion of the version. | |
346 | ||
347 | >>> Version("1.2.3").public | |
348 | '1.2.3' | |
349 | >>> Version("1.2.3+abc").public | |
350 | '1.2.3' | |
351 | >>> Version("1.2.3+abc.dev1").public | |
352 | '1.2.3' | |
353 | """ | |
354 | return str(self).split("+", 1)[0] | |
355 | ||
356 | @property | |
357 | def base_version(self) -> str: | |
358 | """The "base version" of the version. | |
359 | ||
360 | >>> Version("1.2.3").base_version | |
361 | '1.2.3' | |
362 | >>> Version("1.2.3+abc").base_version | |
363 | '1.2.3' | |
364 | >>> Version("1!1.2.3+abc.dev1").base_version | |
365 | '1!1.2.3' | |
366 | ||
367 | The "base version" is the public version of the project without any pre or post | |
368 | release markers. | |
369 | """ | |
370 | parts = [] | |
371 | ||
372 | # Epoch | |
373 | if self.epoch != 0: | |
374 | parts.append(f"{self.epoch}!") | |
375 | ||
376 | # Release segment | |
377 | parts.append(".".join(str(x) for x in self.release)) | |
378 | ||
379 | return "".join(parts) | |
380 | ||
381 | @property | |
382 | def is_prerelease(self) -> bool: | |
383 | """Whether this version is a pre-release. | |
384 | ||
385 | >>> Version("1.2.3").is_prerelease | |
386 | False | |
387 | >>> Version("1.2.3a1").is_prerelease | |
388 | True | |
389 | >>> Version("1.2.3b1").is_prerelease | |
390 | True | |
391 | >>> Version("1.2.3rc1").is_prerelease | |
392 | True | |
393 | >>> Version("1.2.3dev1").is_prerelease | |
394 | True | |
395 | """ | |
396 | return self.dev is not None or self.pre is not None | |
397 | ||
398 | @property | |
399 | def is_postrelease(self) -> bool: | |
400 | """Whether this version is a post-release. | |
401 | ||
402 | >>> Version("1.2.3").is_postrelease | |
403 | False | |
404 | >>> Version("1.2.3.post1").is_postrelease | |
405 | True | |
406 | """ | |
407 | return self.post is not None | |
408 | ||
409 | @property | |
410 | def is_devrelease(self) -> bool: | |
411 | """Whether this version is a development release. | |
412 | ||
413 | >>> Version("1.2.3").is_devrelease | |
414 | False | |
415 | >>> Version("1.2.3.dev1").is_devrelease | |
416 | True | |
417 | """ | |
418 | return self.dev is not None | |
419 | ||
420 | @property | |
421 | def major(self) -> int: | |
422 | """The first item of :attr:`release` or ``0`` if unavailable. | |
423 | ||
424 | >>> Version("1.2.3").major | |
425 | 1 | |
426 | """ | |
427 | return self.release[0] if len(self.release) >= 1 else 0 | |
428 | ||
429 | @property | |
430 | def minor(self) -> int: | |
431 | """The second item of :attr:`release` or ``0`` if unavailable. | |
432 | ||
433 | >>> Version("1.2.3").minor | |
434 | 2 | |
435 | >>> Version("1").minor | |
436 | 0 | |
437 | """ | |
438 | return self.release[1] if len(self.release) >= 2 else 0 | |
439 | ||
440 | @property | |
441 | def micro(self) -> int: | |
442 | """The third item of :attr:`release` or ``0`` if unavailable. | |
443 | ||
444 | >>> Version("1.2.3").micro | |
445 | 3 | |
446 | >>> Version("1").micro | |
447 | 0 | |
448 | """ | |
449 | return self.release[2] if len(self.release) >= 3 else 0 | |
450 | ||
451 | ||
452 | def _parse_letter_version( | |
453 | letter: Optional[str], number: Union[str, bytes, SupportsInt, None] | |
454 | ) -> Optional[Tuple[str, int]]: | |
455 | ||
456 | if letter: | |
457 | # We consider there to be an implicit 0 in a pre-release if there is | |
458 | # not a numeral associated with it. | |
459 | if number is None: | |
460 | number = 0 | |
461 | ||
462 | # We normalize any letters to their lower case form | |
463 | letter = letter.lower() | |
464 | ||
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 | |
467 | # spelling. | |
468 | if letter == "alpha": | |
469 | letter = "a" | |
470 | elif letter == "beta": | |
471 | letter = "b" | |
472 | elif letter in ["c", "pre", "preview"]: | |
473 | letter = "rc" | |
474 | elif letter in ["rev", "r"]: | |
475 | letter = "post" | |
476 | ||
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) | |
481 | letter = "post" | |
482 | ||
483 | return letter, int(number) | |
484 | ||
485 | return None | |
486 | ||
487 | ||
488 | _local_version_separators = re.compile(r"[\._-]") | |
489 | ||
490 | ||
491 | def _parse_local_version(local: Optional[str]) -> Optional[LocalType]: | |
492 | """ | |
493 | Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). | |
494 | """ | |
495 | if local is not None: | |
496 | return tuple( | |
497 | part.lower() if not part.isdigit() else int(part) | |
498 | for part in _local_version_separators.split(local) | |
499 | ) | |
500 | return None | |
501 | ||
502 | ||
503 | def _cmpkey( | |
504 | epoch: int, | |
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], | |
510 | ) -> CmpKey: | |
511 | ||
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. | |
517 | _release = tuple( | |
518 | reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) | |
519 | ) | |
520 | ||
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 | |
528 | # those with one. | |
529 | elif pre is None: | |
530 | _pre = Infinity | |
531 | else: | |
532 | _pre = pre | |
533 | ||
534 | # Versions without a post segment should sort before those with one. | |
535 | if post is None: | |
536 | _post: CmpPrePostDevType = NegativeInfinity | |
537 | ||
538 | else: | |
539 | _post = post | |
540 | ||
541 | # Versions without a development segment should sort after those with one. | |
542 | if dev is None: | |
543 | _dev: CmpPrePostDevType = Infinity | |
544 | ||
545 | else: | |
546 | _dev = dev | |
547 | ||
548 | if local is None: | |
549 | # Versions without a local segment should sort before those with one. | |
550 | _local: CmpLocalType = NegativeInfinity | |
551 | else: | |
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 | |
558 | # match exactly | |
559 | _local = tuple( | |
560 | (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local | |
561 | ) | |
562 | ||
563 | return epoch, _release, _pre, _post, _dev, _local |