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