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.
4from __future__ import absolute_import, division, print_function
5
6import collections
7import itertools
8import re
9
10from ._structures import Infinity
11
12
13__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
14
15
16_Version = collections.namedtuple(
17 "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
18)
19
20
21def parse(version):
22 """
23 Parse the given version string and return either a :class:`Version` object
24 or a :class:`LegacyVersion` object depending on if the given version is
25 a valid PEP 440 version or a legacy version.
26 """
27 try:
28 return Version(version)
29 except InvalidVersion:
30 return LegacyVersion(version)
31
32
33class InvalidVersion(ValueError):
34 """
35 An invalid version was found, users should refer to PEP 440.
36 """
37
38
39class _BaseVersion(object):
40 def __hash__(self):
41 return hash(self._key)
42
43 def __lt__(self, other):
44 return self._compare(other, lambda s, o: s < o)
45
46 def __le__(self, other):
47 return self._compare(other, lambda s, o: s <= o)
48
49 def __eq__(self, other):
50 return self._compare(other, lambda s, o: s == o)
51
52 def __ge__(self, other):
53 return self._compare(other, lambda s, o: s >= o)
54
55 def __gt__(self, other):
56 return self._compare(other, lambda s, o: s > o)
57
58 def __ne__(self, other):
59 return self._compare(other, lambda s, o: s != o)
60
61 def _compare(self, other, method):
62 if not isinstance(other, _BaseVersion):
63 return NotImplemented
64
65 return method(self._key, other._key)
66
67
68class LegacyVersion(_BaseVersion):
69 def __init__(self, version):
70 self._version = str(version)
71 self._key = _legacy_cmpkey(self._version)
72
73 def __str__(self):
74 return self._version
75
76 def __repr__(self):
77 return "<LegacyVersion({0})>".format(repr(str(self)))
78
79 @property
80 def public(self):
81 return self._version
82
83 @property
84 def base_version(self):
85 return self._version
86
87 @property
88 def epoch(self):
89 return -1
90
91 @property
92 def release(self):
93 return None
94
95 @property
96 def pre(self):
97 return None
98
99 @property
100 def post(self):
101 return None
102
103 @property
104 def dev(self):
105 return None
106
107 @property
108 def local(self):
109 return None
110
111 @property
112 def is_prerelease(self):
113 return False
114
115 @property
116 def is_postrelease(self):
117 return False
118
119 @property
120 def is_devrelease(self):
121 return False
122
123
124_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
125
126_legacy_version_replacement_map = {
127 "pre": "c",
128 "preview": "c",
129 "-": "final-",
130 "rc": "c",
131 "dev": "@",
132}
133
134
135def _parse_version_parts(s):
136 for part in _legacy_version_component_re.split(s):
137 part = _legacy_version_replacement_map.get(part, part)
138
139 if not part or part == ".":
140 continue
141
142 if part[:1] in "0123456789":
143 # pad for numeric comparison
144 yield part.zfill(8)
145 else:
146 yield "*" + part
147
148 # ensure that alpha/beta/candidate are before final
149 yield "*final"
150
151
152def _legacy_cmpkey(version):
153 # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
154 # greater than or equal to 0. This will effectively put the LegacyVersion,
155 # which uses the defacto standard originally implemented by setuptools,
156 # as before all PEP 440 versions.
157 epoch = -1
158
159 # This scheme is taken from pkg_resources.parse_version setuptools prior to
160 # it's adoption of the packaging library.
161 parts = []
162 for part in _parse_version_parts(version.lower()):
163 if part.startswith("*"):
164 # remove "-" before a prerelease tag
165 if part < "*final":
166 while parts and parts[-1] == "*final-":
167 parts.pop()
168
169 # remove trailing zeros from each series of numeric parts
170 while parts and parts[-1] == "00000000":
171 parts.pop()
172
173 parts.append(part)
174 parts = tuple(parts)
175
176 return epoch, parts
177
178
179# Deliberately not anchored to the start and end of the string, to make it
180# easier for 3rd party code to reuse
181VERSION_PATTERN = r"""
182 v?
183 (?:
184 (?:(?P<epoch>[0-9]+)!)? # epoch
185 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
186 (?P<pre> # pre-release
187 [-_\.]?
188 (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
189 [-_\.]?
190 (?P<pre_n>[0-9]+)?
191 )?
192 (?P<post> # post release
193 (?:-(?P<post_n1>[0-9]+))
194 |
195 (?:
196 [-_\.]?
197 (?P<post_l>post|rev|r)
198 [-_\.]?
199 (?P<post_n2>[0-9]+)?
200 )
201 )?
202 (?P<dev> # dev release
203 [-_\.]?
204 (?P<dev_l>dev)
205 [-_\.]?
206 (?P<dev_n>[0-9]+)?
207 )?
208 )
209 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
210"""
211
212
213class Version(_BaseVersion):
214
215 _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
216
217 def __init__(self, version):
218 # Validate the version and parse it into pieces
219 match = self._regex.search(version)
220 if not match:
221 raise InvalidVersion("Invalid version: '{0}'".format(version))
222
223 # Store the parsed out pieces of the version
224 self._version = _Version(
225 epoch=int(match.group("epoch")) if match.group("epoch") else 0,
226 release=tuple(int(i) for i in match.group("release").split(".")),
227 pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
228 post=_parse_letter_version(
229 match.group("post_l"), match.group("post_n1") or match.group("post_n2")
230 ),
231 dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
232 local=_parse_local_version(match.group("local")),
233 )
234
235 # Generate a key which will be used for sorting
236 self._key = _cmpkey(
237 self._version.epoch,
238 self._version.release,
239 self._version.pre,
240 self._version.post,
241 self._version.dev,
242 self._version.local,
243 )
244
245 def __repr__(self):
246 return "<Version({0})>".format(repr(str(self)))
247
248 def __str__(self):
249 parts = []
250
251 # Epoch
252 if self.epoch != 0:
253 parts.append("{0}!".format(self.epoch))
254
255 # Release segment
256 parts.append(".".join(str(x) for x in self.release))
257
258 # Pre-release
259 if self.pre is not None:
260 parts.append("".join(str(x) for x in self.pre))
261
262 # Post-release
263 if self.post is not None:
264 parts.append(".post{0}".format(self.post))
265
266 # Development release
267 if self.dev is not None:
268 parts.append(".dev{0}".format(self.dev))
269
270 # Local version segment
271 if self.local is not None:
272 parts.append("+{0}".format(self.local))
273
274 return "".join(parts)
275
276 @property
277 def epoch(self):
278 return self._version.epoch
279
280 @property
281 def release(self):
282 return self._version.release
283
284 @property
285 def pre(self):
286 return self._version.pre
287
288 @property
289 def post(self):
290 return self._version.post[1] if self._version.post else None
291
292 @property
293 def dev(self):
294 return self._version.dev[1] if self._version.dev else None
295
296 @property
297 def local(self):
298 if self._version.local:
299 return ".".join(str(x) for x in self._version.local)
300 else:
301 return None
302
303 @property
304 def public(self):
305 return str(self).split("+", 1)[0]
306
307 @property
308 def base_version(self):
309 parts = []
310
311 # Epoch
312 if self.epoch != 0:
313 parts.append("{0}!".format(self.epoch))
314
315 # Release segment
316 parts.append(".".join(str(x) for x in self.release))
317
318 return "".join(parts)
319
320 @property
321 def is_prerelease(self):
322 return self.dev is not None or self.pre is not None
323
324 @property
325 def is_postrelease(self):
326 return self.post is not None
327
328 @property
329 def is_devrelease(self):
330 return self.dev is not None
331
332
333def _parse_letter_version(letter, number):
334 if letter:
335 # We consider there to be an implicit 0 in a pre-release if there is
336 # not a numeral associated with it.
337 if number is None:
338 number = 0
339
340 # We normalize any letters to their lower case form
341 letter = letter.lower()
342
343 # We consider some words to be alternate spellings of other words and
344 # in those cases we want to normalize the spellings to our preferred
345 # spelling.
346 if letter == "alpha":
347 letter = "a"
348 elif letter == "beta":
349 letter = "b"
350 elif letter in ["c", "pre", "preview"]:
351 letter = "rc"
352 elif letter in ["rev", "r"]:
353 letter = "post"
354
355 return letter, int(number)
356 if not letter and number:
357 # We assume if we are given a number, but we are not given a letter
358 # then this is using the implicit post release syntax (e.g. 1.0-1)
359 letter = "post"
360
361 return letter, int(number)
362
363
364_local_version_separators = re.compile(r"[\._-]")
365
366
367def _parse_local_version(local):
368 """
369 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
370 """
371 if local is not None:
372 return tuple(
373 part.lower() if not part.isdigit() else int(part)
374 for part in _local_version_separators.split(local)
375 )
376
377
378def _cmpkey(epoch, release, pre, post, dev, local):
379 # When we compare a release version, we want to compare it with all of the
380 # trailing zeros removed. So we'll use a reverse the list, drop all the now
381 # leading zeros until we come to something non zero, then take the rest
382 # re-reverse it back into the correct order and make it a tuple and use
383 # that for our sorting key.
384 release = tuple(
385 reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
386 )
387
388 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
389 # We'll do this by abusing the pre segment, but we _only_ want to do this
390 # if there is not a pre or a post segment. If we have one of those then
391 # the normal sorting rules will handle this case correctly.
392 if pre is None and post is None and dev is not None:
393 pre = -Infinity
394 # Versions without a pre-release (except as noted above) should sort after
395 # those with one.
396 elif pre is None:
397 pre = Infinity
398
399 # Versions without a post segment should sort before those with one.
400 if post is None:
401 post = -Infinity
402
403 # Versions without a development segment should sort after those with one.
404 if dev is None:
405 dev = Infinity
406
407 if local is None:
408 # Versions without a local segment should sort before those with one.
409 local = -Infinity
410 else:
411 # Versions with a local segment need that segment parsed to implement
412 # the sorting rules in PEP440.
413 # - Alpha numeric segments sort before numeric segments
414 # - Alpha numeric segments sort lexicographically
415 # - Numeric segments sort numerically
416 # - Shorter versions sort before longer versions when the prefixes
417 # match exactly
418 local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local)
419
420 return epoch, release, pre, post, dev, local