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