| 1 | # Copyright (C) 2010, OLPC |
| 2 | # |
| 3 | # This library is free software; you can redistribute it and/or |
| 4 | # modify it under the terms of the GNU Lesser General Public |
| 5 | # License as published by the Free Software Foundation; either |
| 6 | # version 2 of the License, or (at your option) any later version. |
| 7 | # |
| 8 | # This library is distributed in the hope that it will be useful, |
| 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 11 | # Lesser General Public License for more details. |
| 12 | # |
| 13 | # You should have received a copy of the GNU Lesser General Public |
| 14 | # License along with this library; if not, write to the |
| 15 | # Free Software Foundation, Inc., 59 Temple Place - Suite 330, |
| 16 | # Boston, MA 02111-1307, USA. |
| 17 | |
| 18 | # |
| 19 | # Based in the implementation of PEP 386, but adapted to our numeration schema |
| 20 | # |
| 21 | |
| 22 | import re |
| 23 | |
| 24 | class InvalidVersionError(Exception): |
| 25 | """This is an not normalized version.""" |
| 26 | pass |
| 27 | |
| 28 | VERSION_RE = re.compile(r''' |
| 29 | ^ |
| 30 | (?P<version>\d+) # minimum 'N' |
| 31 | (?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments |
| 32 | (?: |
| 33 | (?P<local>\-[a-zA-Z]*) # any string, will be ignored in the comparison |
| 34 | )? |
| 35 | $''', re.VERBOSE) |
| 36 | |
| 37 | class NormalizedVersion(object): |
| 38 | """A normalized version. |
| 39 | |
| 40 | Good: |
| 41 | 1 |
| 42 | 1.2 |
| 43 | 1.2.3 |
| 44 | 1.2.3-peru |
| 45 | |
| 46 | Bad: |
| 47 | 1.2peru # must separate with - |
| 48 | 1.2. # can't end with . |
| 49 | 1.02.5 # can't have a leading zero |
| 50 | """ |
| 51 | def __init__(self, s, error_on_huge_major_num=True): |
| 52 | """Create a NormalizedVersion instance from a version string. |
| 53 | |
| 54 | @param s {str} The version string. |
| 55 | @param error_on_huge_major_num {bool} Whether to consider an |
| 56 | apparent use of a year or full date as the major version number |
| 57 | an error. Default True. One of the observed patterns on PyPI before |
| 58 | the introduction of `NormalizedVersion` was version numbers like this: |
| 59 | 2009.01.03 |
| 60 | 20040603 |
| 61 | 2005.01 |
| 62 | This guard is here to strongly encourage the package author to |
| 63 | use an alternate version, because a release deployed into PyPI |
| 64 | and, e.g. downstream Linux package managers, will forever remove |
| 65 | the possibility of using a version number like "1.0" (i.e. |
| 66 | where the major number is less than that huge major number). |
| 67 | """ |
| 68 | match = VERSION_RE.search(s) |
| 69 | if not match: |
| 70 | raise InvalidVersionError(s) |
| 71 | |
| 72 | groups = match.groupdict() |
| 73 | parts = [] |
| 74 | |
| 75 | # main version |
| 76 | block = self._parse_numdots(groups['version'], s, False, 1) |
| 77 | extraversion = groups.get('extraversion') |
| 78 | if extraversion not in ('', None): |
| 79 | block += self._parse_numdots(extraversion[1:], s) |
| 80 | parts.extend(block) |
| 81 | self._parts = parts |
| 82 | self._local = groups['local'] |
| 83 | |
| 84 | def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True, |
| 85 | pad_zeros_length=0): |
| 86 | """Parse 'N.N.N' sequences, return a list of ints. |
| 87 | |
| 88 | @param s {str} 'N.N.N..." sequence to be parsed |
| 89 | @param full_ver_str {str} The full version string from which this |
| 90 | comes. Used for error strings. |
| 91 | @param drop_trailing_zeros {bool} Whether to drop trailing zeros |
| 92 | from the returned list. Default True. |
| 93 | @param pad_zeros_length {int} The length to which to pad the |
| 94 | returned list with zeros, if necessary. Default 0. |
| 95 | """ |
| 96 | nums = [] |
| 97 | for n in s.split("."): |
| 98 | if len(n) > 1 and n[0] == '0': |
| 99 | raise InvalidVersionError("cannot have leading zero in " |
| 100 | "version number segment: '%s' in %r" % (n, full_ver_str)) |
| 101 | nums.append(int(n)) |
| 102 | if drop_trailing_zeros: |
| 103 | while nums and nums[-1] == 0: |
| 104 | nums.pop() |
| 105 | while len(nums) < pad_zeros_length: |
| 106 | nums.append(0) |
| 107 | return nums |
| 108 | |
| 109 | def get_parts(self): |
| 110 | return self._parts |
| 111 | |
| 112 | def __str__(self): |
| 113 | s = self._parts_to_str(self._parts) |
| 114 | if self._local != None: |
| 115 | s = s + self._local |
| 116 | return s |
| 117 | |
| 118 | @classmethod |
| 119 | def _parts_to_str(cls, parts): |
| 120 | """Transforms a version expressed in tuple into its string |
| 121 | representation.""" |
| 122 | main = parts |
| 123 | s = '.'.join(str(v) for v in main) |
| 124 | return s |
| 125 | |
| 126 | def __repr__(self): |
| 127 | return "%s('%s')" % (self.__class__.__name__, self) |
| 128 | |
| 129 | def _cannot_compare(self, other): |
| 130 | raise TypeError("cannot compare %s and %s" |
| 131 | % (type(self).__name__, type(other).__name__)) |
| 132 | |
| 133 | def __eq__(self, other): |
| 134 | if not isinstance(other, NormalizedVersion): |
| 135 | self._cannot_compare(other) |
| 136 | return self._parts == other.get_parts() |
| 137 | |
| 138 | def __lt__(self, other): |
| 139 | if not isinstance(other, NormalizedVersion): |
| 140 | self._cannot_compare(other) |
| 141 | return self._parts < other.get_parts() |
| 142 | |
| 143 | def __ne__(self, other): |
| 144 | return not self.__eq__(other) |
| 145 | |
| 146 | def __gt__(self, other): |
| 147 | return not (self.__lt__(other) or self.__eq__(other)) |
| 148 | |
| 149 | def __le__(self, other): |
| 150 | return self.__eq__(other) or self.__lt__(other) |
| 151 | |
| 152 | def __ge__(self, other): |
| 153 | return self.__eq__(other) or self.__gt__(other) |
| 154 | |