Attachment 'verlib.py'
Download 1 r"""
2 "Rational" version definition and parsing for DistutilsVersionFight
3 discussion at PyCon 2009.
4
5 >>> from verlib import RationalVersion as V
6 >>> v = V('1.2.3')
7 >>> str(v)
8 '1.2.3'
9
10 >>> v = V('1.2')
11 >>> str(v)
12 '1.2'
13
14 >>> v = V('1.2.3a4')
15 >>> str(v)
16 '1.2.3a4'
17
18 >>> v = V('1.2c4')
19 >>> str(v)
20 '1.2c4'
21
22 >>> v = V('1.2.3.4')
23 >>> str(v)
24 '1.2.3.4'
25
26 >>> v = V('1.2.3.4.0b3')
27 >>> str(v)
28 '1.2.3.4b3'
29
30 >>> V('1.2.0.0.0') == V('1.2')
31 True
32
33 # Irrational version strings
34
35 >>> v = V('1')
36 Traceback (most recent call last):
37 ...
38 IrrationalVersionError: 1
39 >>> v = V('1.2a')
40 Traceback (most recent call last):
41 ...
42 IrrationalVersionError: 1.2a
43 >>> v = V('1.2.3b')
44 Traceback (most recent call last):
45 ...
46 IrrationalVersionError: 1.2.3b
47 >>> v = V('1.02')
48 Traceback (most recent call last):
49 ...
50 IrrationalVersionError: cannot have leading zero in version number segment: '02' in '1.02'
51 >>> v = V('1.2a03')
52 Traceback (most recent call last):
53 ...
54 IrrationalVersionError: cannot have leading zero in version number segment: '03' in '1.2a03'
55 >>> v = V('1.2a3.04')
56 Traceback (most recent call last):
57 ...
58 IrrationalVersionError: cannot have leading zero in version number segment: '04' in '1.2a3.04'
59
60 # Comparison
61
62 >>> V('1.2.0') == '1.2'
63 Traceback (most recent call last):
64 ...
65 TypeError: cannot compare RationalVersion and str
66
67 >>> V('1.2.0') == V('1.2')
68 True
69 >>> V('1.2.0') == V('1.2.3')
70 False
71 >>> V('1.2.0') < V('1.2.3')
72 True
73 >>> (V('1.0') > V('1.0b2'))
74 True
75 >>> (V('1.0') > V('1.0c2') > V('1.0c1') > V('1.0b2') > V('1.0b1')
76 ... > V('1.0a2') > V('1.0a1'))
77 True
78 >>> (V('1.0.0') > V('1.0.0c2') > V('1.0.0c1') > V('1.0.0b2') > V('1.0.0b1')
79 ... > V('1.0.0a2') > V('1.0.0a1'))
80 True
81
82 >>> (V('1.0a1')
83 ... < V('1.0a2.dev456')
84 ... < V('1.0a2')
85 ... < V('1.0a2.1.dev456') # e.g. need to do a quick post release on 1.0a2
86 ... < V('1.0a2.1')
87 ... < V('1.0b1.dev456')
88 ... < V('1.0b2')
89 ... < V('1.0c1.dev456')
90 ... < V('1.0c1')
91 ... < V('1.0.dev456')
92 ... < V('1.0')
93 ... < V('1.0.post456'))
94 True
95
96 """
97
98 import sys
99 import re
100 from pprint import pprint
101
102
103 class IrrationalVersionError(Exception):
104 """This is an irrational version."""
105
106
107 class RationalVersion(object):
108 """A rational version.
109
110 Good:
111 1.2 # equivalent to "1.2.0"
112 1.2.0
113 1.2a1
114 1.2.3a2
115 1.2.3b1
116 1.2.3c1
117 1.2.3.4
118 TODO: fill this out
119
120 Bad:
121 1 # mininum two numbers
122 1.2a # release level must have a release serial
123 1.2.3b
124 """
125 def __init__(self, s):
126 self._parse(s)
127
128 _version_re = re.compile(r'''
129 ^
130 (\d+\.\d+) # minimum 'N.N'
131 ((?:\.\d+)*) # any number of extra '.N' segments
132 (?:
133 ([abc]) # 'a'=alpha, 'b'=beta, 'c'=release candidate
134 (\d+(?:\.\d+)*)
135 )?
136 (\.(dev|post)(\d+))? # pre- (aka development) and post-release tag
137 $
138 ''', re.VERBOSE)
139 # A marker used in the second and third parts of the `info` tuple, for
140 # versions that don't have those segments, to sort properly. A example
141 # of versions in sort order ('highest' last):
142 # 1.0b1 ((1,0), ('b',1), ('f',))
143 # 1.0.dev345 ((1,0), ('f',), ('dev', 345))
144 # 1.0 ((1,0), ('f',), ('f',))
145 # 1.0.post345 ((1,0), ('f',), ('post', 345))
146 # ^ ^
147 # 'f' < 'b' -------------/ |
148 # |
149 # 'dev' < 'f' < 'post' -----------/
150 # Other letters would do, bug 'f' for 'final' is kind of nice.
151 _final_marker = ('f',)
152
153 def _parse(self, s):
154 match = self._version_re.search(s)
155 if not match:
156 raise IrrationalVersionError(s)
157 groups = match.groups()
158 parts = []
159 block = self._parse_numdots(groups[0], s, False, 2)
160 if groups[1]:
161 block += self._parse_numdots(groups[1][1:], s)
162 parts.append(tuple(block))
163 if groups[2]:
164 block = [groups[2]]
165 block += self._parse_numdots(groups[3], s, pad_zeros_length=1)
166 parts.append(tuple(block))
167 else:
168 parts.append(self._final_marker)
169 if groups[4]:
170 parts.append((groups[5], int(groups[6])))
171 else:
172 parts.append(self._final_marker)
173 self.info = tuple(parts)
174 #print "_parse(%r) -> %r" % (s, self.info)
175
176 def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True,
177 pad_zeros_length=0):
178 """Parse 'N.N.N' sequences, return a list of ints.
179
180 @param s {str} 'N.N.N..." sequence to be parsed
181 @param full_ver_str {str} The full version string from which this
182 comes. Used for error strings.
183 @param drop_trailing_zeros {bool} Whether to drop trailing zeros
184 from the returned list. Default True.
185 @param pad_zeros_length {int} The length to which to pad the
186 returned list with zeros, if necessary. Default 0.
187 """
188 nums = []
189 for n in s.split("."):
190 if len(n) > 1 and n[0] == '0':
191 raise IrrationalVersionError("cannot have leading zero in "
192 "version number segment: '%s' in %r" % (n, full_ver_str))
193 nums.append(int(n))
194 if drop_trailing_zeros:
195 while nums and nums[-1] == 0:
196 nums.pop()
197 while len(nums) < pad_zeros_length:
198 nums.append(0)
199 return nums
200
201 def __cmp__(self, other):
202 if not isinstance(other, RationalVersion):
203 raise TypeError("cannot compare %s and %s"
204 % (type(self).__name__, type(other).__name__))
205 return cmp(self.info, other.info)
206
207 def __str__(self):
208 main, prerel, devpost = self.info
209 s = '.'.join(str(v) for v in main if v)
210 if prerel is not self._final_marker:
211 s += prerel[0]
212 s += '.'.join(str(v) for v in prerel[1:] if v)
213 if devpost is not self._final_marker:
214 s += '.' + ''.join(str(v) for v in prerel[1:] if v)
215 return s
216
217 def __repr__(self):
218 return "%s('%s')" % (self.__class__.__name__, self)
219
220
221 #---- mainline and test
222
223 def _test():
224 import doctest
225 doctest.testmod()
226
227 def _play():
228 V = RationalVersion
229 print V('1.0.dev123') < V('1.0.dev456') < V('1.0') < V('1.0.post456') < V('1.0.post789')
230
231 if __name__ == "__main__":
232 #_play()
233 _test()
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.