comparison rhodecode/lib/verlib.py @ 4116:ffd45b185016 rhodecode-2.2.5-gpl

Imported some of the GPLv3'd changes from RhodeCode v2.2.5. This imports changes between changesets 21af6c4eab3d and 6177597791c2 in RhodeCode's original repository, including only changes to Python files and HTML. RhodeCode clearly licensed its changes to these files under GPLv3 in their /LICENSE file, which states the following: The Python code and integrated HTML are licensed under the GPLv3 license. (See: https://code.rhodecode.com/rhodecode/files/v2.2.5/LICENSE or http://web.archive.org/web/20140512193334/https://code.rhodecode.com/rhodecode/files/f3b123159901f15426d18e3dc395e8369f70ebe0/LICENSE for an online copy of that LICENSE file) Conservancy reviewed these changes and confirmed that they can be licensed as a whole to the Kallithea project under GPLv3-only. While some of the contents committed herein are clearly licensed GPLv3-or-later, on the whole we must assume the are GPLv3-only, since the statement above from RhodeCode indicates that they intend GPLv3-only as their license, per GPLv3ยง14 and other relevant sections of GPLv3.
author Bradley M. Kuhn <bkuhn@sfconservancy.org>
date Wed, 02 Jul 2014 19:03:13 -0400
parents
children
comparison
equal deleted inserted replaced
4115:8b7294a804a0 4116:ffd45b185016
1 """
2 "Rational" version definition and parsing for DistutilsVersionFight
3 discussion at PyCon 2009.
4 """
5
6 import sys
7 import re
8
9 class IrrationalVersionError(Exception):
10 """This is an irrational version."""
11 pass
12
13 class HugeMajorVersionNumError(IrrationalVersionError):
14 """An irrational version because the major version number is huge
15 (often because a year or date was used).
16
17 See `error_on_huge_major_num` option in `NormalizedVersion` for details.
18 This guard can be disabled by setting that option False.
19 """
20 pass
21
22 # A marker used in the second and third parts of the `parts` tuple, for
23 # versions that don't have those segments, to sort properly. An example
24 # of versions in sort order ('highest' last):
25 # 1.0b1 ((1,0), ('b',1), ('f',))
26 # 1.0.dev345 ((1,0), ('f',), ('dev', 345))
27 # 1.0 ((1,0), ('f',), ('f',))
28 # 1.0.post256.dev345 ((1,0), ('f',), ('f', 'post', 256, 'dev', 345))
29 # 1.0.post345 ((1,0), ('f',), ('f', 'post', 345, 'f'))
30 # ^ ^ ^
31 # 'b' < 'f' ---------------------/ | |
32 # | |
33 # 'dev' < 'f' < 'post' -------------------/ |
34 # |
35 # 'dev' < 'f' ----------------------------------------------/
36 # Other letters would do, but 'f' for 'final' is kind of nice.
37 FINAL_MARKER = ('f',)
38
39 VERSION_RE = re.compile(r'''
40 ^
41 (?P<version>\d+\.\d+) # minimum 'N.N'
42 (?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments
43 (?:
44 (?P<prerel>[abc]|rc) # 'a'=alpha, 'b'=beta, 'c'=release candidate
45 # 'rc'= alias for release candidate
46 (?P<prerelversion>\d+(?:\.\d+)*)
47 )?
48 (?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
49 $''', re.VERBOSE)
50
51 class NormalizedVersion(object):
52 """A rational version.
53
54 Good:
55 1.2 # equivalent to "1.2.0"
56 1.2.0
57 1.2a1
58 1.2.3a2
59 1.2.3b1
60 1.2.3c1
61 1.2.3.4
62 TODO: fill this out
63
64 Bad:
65 1 # mininum two numbers
66 1.2a # release level must have a release serial
67 1.2.3b
68 """
69 def __init__(self, s, error_on_huge_major_num=True):
70 """Create a NormalizedVersion instance from a version string.
71
72 :param s {str} The version string.
73 :param error_on_huge_major_num {bool} Whether to consider an
74 apparent use of a year or full date as the major version number
75 an error. Default True. One of the observed patterns on PyPI before
76 the introduction of `NormalizedVersion` was version numbers like this:
77 2009.01.03
78 20040603
79 2005.01
80 This guard is here to strongly encourage the package author to
81 use an alternate version, because a release deployed into PyPI
82 and, e.g. downstream Linux package managers, will forever remove
83 the possibility of using a version number like "1.0" (i.e.
84 where the major number is less than that huge major number).
85 """
86 self._parse(s, error_on_huge_major_num)
87
88 @classmethod
89 def from_parts(cls, version, prerelease=FINAL_MARKER,
90 devpost=FINAL_MARKER):
91 return cls(cls.parts_to_str((version, prerelease, devpost)))
92
93 def _parse(self, s, error_on_huge_major_num=True):
94 """Parses a string version into parts."""
95 match = VERSION_RE.search(s)
96 if not match:
97 raise IrrationalVersionError(s)
98
99 groups = match.groupdict()
100 parts = []
101
102 # main version
103 block = self._parse_numdots(groups['version'], s, False, 2)
104 extraversion = groups.get('extraversion')
105 if extraversion not in ('', None):
106 block += self._parse_numdots(extraversion[1:], s)
107 parts.append(tuple(block))
108
109 # prerelease
110 prerel = groups.get('prerel')
111 if prerel is not None:
112 block = [prerel]
113 block += self._parse_numdots(groups.get('prerelversion'), s,
114 pad_zeros_length=1)
115 parts.append(tuple(block))
116 else:
117 parts.append(FINAL_MARKER)
118
119 # postdev
120 if groups.get('postdev'):
121 post = groups.get('post')
122 dev = groups.get('dev')
123 postdev = []
124 if post is not None:
125 postdev.extend([FINAL_MARKER[0], 'post', int(post)])
126 if dev is None:
127 postdev.append(FINAL_MARKER[0])
128 if dev is not None:
129 postdev.extend(['dev', int(dev)])
130 parts.append(tuple(postdev))
131 else:
132 parts.append(FINAL_MARKER)
133 self.parts = tuple(parts)
134 if error_on_huge_major_num and self.parts[0][0] > 1980:
135 raise HugeMajorVersionNumError("huge major version number, %r, "
136 "which might cause future problems: %r" % (self.parts[0][0], s))
137
138 def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True,
139 pad_zeros_length=0):
140 """Parse 'N.N.N' sequences, return a list of ints.
141
142 :param s {str} 'N.N.N..." sequence to be parsed
143 :param full_ver_str {str} The full version string from which this
144 comes. Used for error strings.
145 :param drop_trailing_zeros {bool} Whether to drop trailing zeros
146 from the returned list. Default True.
147 :param pad_zeros_length {int} The length to which to pad the
148 returned list with zeros, if necessary. Default 0.
149 """
150 nums = []
151 for n in s.split("."):
152 if len(n) > 1 and n[0] == '0':
153 raise IrrationalVersionError("cannot have leading zero in "
154 "version number segment: '%s' in %r" % (n, full_ver_str))
155 nums.append(int(n))
156 if drop_trailing_zeros:
157 while nums and nums[-1] == 0:
158 nums.pop()
159 while len(nums) < pad_zeros_length:
160 nums.append(0)
161 return nums
162
163 def __str__(self):
164 return self.parts_to_str(self.parts)
165
166 @classmethod
167 def parts_to_str(cls, parts):
168 """Transforms a version expressed in tuple into its string
169 representation."""
170 # XXX This doesn't check for invalid tuples
171 main, prerel, postdev = parts
172 s = '.'.join(str(v) for v in main)
173 if prerel is not FINAL_MARKER:
174 s += prerel[0]
175 s += '.'.join(str(v) for v in prerel[1:])
176 if postdev and postdev is not FINAL_MARKER:
177 if postdev[0] == 'f':
178 postdev = postdev[1:]
179 i = 0
180 while i < len(postdev):
181 if i % 2 == 0:
182 s += '.'
183 s += str(postdev[i])
184 i += 1
185 return s
186
187 def __repr__(self):
188 return "%s('%s')" % (self.__class__.__name__, self)
189
190 def _cannot_compare(self, other):
191 raise TypeError("cannot compare %s and %s"
192 % (type(self).__name__, type(other).__name__))
193
194 def __eq__(self, other):
195 if not isinstance(other, NormalizedVersion):
196 self._cannot_compare(other)
197 return self.parts == other.parts
198
199 def __lt__(self, other):
200 if not isinstance(other, NormalizedVersion):
201 self._cannot_compare(other)
202 return self.parts < other.parts
203
204 def __ne__(self, other):
205 return not self.__eq__(other)
206
207 def __gt__(self, other):
208 return not (self.__lt__(other) or self.__eq__(other))
209
210 def __le__(self, other):
211 return self.__eq__(other) or self.__lt__(other)
212
213 def __ge__(self, other):
214 return self.__eq__(other) or self.__gt__(other)
215
216 def suggest_normalized_version(s):
217 """Suggest a normalized version close to the given version string.
218
219 If you have a version string that isn't rational (i.e. NormalizedVersion
220 doesn't like it) then you might be able to get an equivalent (or close)
221 rational version from this function.
222
223 This does a number of simple normalizations to the given string, based
224 on observation of versions currently in use on PyPI. Given a dump of
225 those version during PyCon 2009, 4287 of them:
226 - 2312 (53.93%) match NormalizedVersion without change
227 - with the automatic suggestion
228 - 3474 (81.04%) match when using this suggestion method
229
230 :param s {str} An irrational version string.
231 :returns: A rational version string, or None, if couldn't determine one.
232 """
233 try:
234 NormalizedVersion(s)
235 return s # already rational
236 except IrrationalVersionError:
237 pass
238
239 rs = s.lower()
240
241 # part of this could use maketrans
242 for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
243 ('beta', 'b'), ('rc', 'c'), ('-final', ''),
244 ('-pre', 'c'),
245 ('-release', ''), ('.release', ''), ('-stable', ''),
246 ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
247 ('final', '')):
248 rs = rs.replace(orig, repl)
249
250 # if something ends with dev or pre, we add a 0
251 rs = re.sub(r"pre$", r"pre0", rs)
252 rs = re.sub(r"dev$", r"dev0", rs)
253
254 # if we have something like "b-2" or "a.2" at the end of the
255 # version, that is pobably beta, alpha, etc
256 # let's remove the dash or dot
257 rs = re.sub(r"([abc|rc])[\-\.](\d+)$", r"\1\2", rs)
258
259 # 1.0-dev-r371 -> 1.0.dev371
260 # 0.1-dev-r79 -> 0.1.dev79
261 rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
262
263 # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
264 rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
265
266 # Clean: v0.3, v1.0
267 if rs.startswith('v'):
268 rs = rs[1:]
269
270 # Clean leading '0's on numbers.
271 #TODO: unintended side-effect on, e.g., "2003.05.09"
272 # PyPI stats: 77 (~2%) better
273 rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
274
275 # Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers
276 # zero.
277 # PyPI stats: 245 (7.56%) better
278 rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
279
280 # the 'dev-rNNN' tag is a dev tag
281 rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
282
283 # clean the - when used as a pre delimiter
284 rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
285
286 # a terminal "dev" or "devel" can be changed into ".dev0"
287 rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
288
289 # a terminal "dev" can be changed into ".dev0"
290 rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
291
292 # a terminal "final" or "stable" can be removed
293 rs = re.sub(r"(final|stable)$", "", rs)
294
295 # The 'r' and the '-' tags are post release tags
296 # 0.4a1.r10 -> 0.4a1.post10
297 # 0.9.33-17222 -> 0.9.3.post17222
298 # 0.9.33-r17222 -> 0.9.3.post17222
299 rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
300
301 # Clean 'r' instead of 'dev' usage:
302 # 0.9.33+r17222 -> 0.9.3.dev17222
303 # 1.0dev123 -> 1.0.dev123
304 # 1.0.git123 -> 1.0.dev123
305 # 1.0.bzr123 -> 1.0.dev123
306 # 0.1a0dev.123 -> 0.1a0.dev123
307 # PyPI stats: ~150 (~4%) better
308 rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
309
310 # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
311 # 0.2.pre1 -> 0.2c1
312 # 0.2-c1 -> 0.2c1
313 # 1.0preview123 -> 1.0c123
314 # PyPI stats: ~21 (0.62%) better
315 rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
316
317
318 # Tcl/Tk uses "px" for their post release markers
319 rs = re.sub(r"p(\d+)$", r".post\1", rs)
320
321 try:
322 NormalizedVersion(rs)
323 return rs # already rational
324 except IrrationalVersionError:
325 pass
326 return None