PageRenderTime 39ms CodeModel.GetById 23ms app.highlight 12ms RepoModel.GetById 0ms app.codeStats 1ms

/pip/_vendor/packaging/version.py

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