Mercurial > kallithea
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 |