Coverage for versio/version_scheme.py : 92%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# coding=utf-8
2"""
3 This class defines the version scheme used by Version.
5 A version scheme consists of a::
7 * name,
8 * regular expression used to parse the version string,
9 * the regular expression flags to use (mainly to allow verbose regexes),
10 * format string used to reassemble the parsed version into a string,
11 * optional list of field types (if not specified, assumes all fields are strings),
12 * list of field names used for accessing the components of the version.
13 * an optional subfield dictionary with the key being a field name and the value being a list of sub field names.
14 For example, in the Pep440VersionScheme, the "Release" field may contain multiple parts, so we use
15 subfield names for the parts. Say we have a version of "1.2.3rc1", the "1.2.3" is the release field, then
16 "1" is the "major" subfield, "2" is the "minor" subfield, and "3" is the "tiny" subfield.
17 * a "clear" value used to set the field values to the right of the field being bumped,
18 * a sequence dictionary where the keys are the field names and the values are a list of allowed values.
19 The list must be in bump order. Bumping the last value in the list has no effect.
21 Note, you need to manually maintain consistency between the regular expression,
22 the format string, the optional field types, and the fields list. For example,
23 if your version scheme has N parts, then the regular expression should match
24 into N groups, the format string should expect N arguments to the str.format()
25 method, and there must be N unique names in the fields list.
26"""
28# noinspection PyUnusedName
29__docformat__ = 'restructuredtext en'
31import re
32from textwrap import dedent
35class AVersionScheme(object):
36 def __init__(self, name, description=None):
37 """
38 The commonality between version schemes.
40 :param name: the name of the versioning scheme.
41 :type name: str
42 :param description: the description of the versioning scheme
43 :type description: str
44 """
45 self.name = name
46 self.description = description or name
47 self.compare_order = None
48 self.compare_fill = None
49 self.format_types = []
50 self.extend_value = '0'
52 # noinspection PyUnusedFunction
53 def parse(self, version_str):
54 """
55 Parse the version using this scheme from the given string. Returns None if unable to parse.
57 :param version_str: A string that may contain a version in this version scheme.
58 :returns: the parts of the version identified with the regular expression or None.
59 :rtype: list of str or None
60 """
61 raise NotImplementedError
64class VersionScheme(AVersionScheme):
65 """Describe a versioning scheme"""
67 def __init__(self, name, parse_regex, clear_value, format_str, format_types=None, fields=None, subfields=None,
68 parse_flags=0, compare_order=None, compare_fill=None, sequences=None, description=None):
69 """
70 A versioning scheme is defined when an instance is created.
71 :param name: the name of the versioning scheme.
72 :type name: str
73 :param parse_regex: the regular expression that parses the version from a string.
74 :type parse_regex: str
75 :param clear_value: the value that the fields to the right of the bumped field get set to.
76 :type clear_value: str or None
77 :param format_str: the format string used to reassemble the version into a string
78 :type format_str: str
79 :param format_types: a list of types used to case the version parts before formatting.
80 :type format_types: list of type
81 :param fields: the list of field names used to access the individual version parts
82 :type fields: list of str
83 :param subfields: a dictionary of field name/list of subfield names use to access parts within a version part
84 :type subfields: dict
85 :param parse_flags: the regular expression flags to use when parsing a version string
86 :type parse_flags: int
87 :param compare_order: The optional list containing the order to compare the parts.
88 :type compare_order: list[int] or None
89 :param compare_fill: The optional list containing the fill string to use when comparing the parts.
90 :type compare_fill: list[str] or None
91 :param sequences: a dictionary of field name/list of values used for sequencing a version part
92 :type sequences: dict
93 :param description: the description of the versioning scheme
94 :type description: str
95 """
96 super(VersionScheme, self).__init__(name=name, description=description)
97 self.parse_regex = parse_regex
98 self.clear_value = clear_value
99 self.format_str = format_str
100 self.format_types = format_types or [] # unspecified format parts are cast to str
101 self.fields = [field.lower() for field in (fields or [])]
102 self.subfields = {}
103 for key in (subfields or {}):
104 for index, field_name in enumerate(subfields[key] or []):
105 self.subfields[field_name.lower()] = [key, index]
107 self.parse_flags = parse_flags
108 self.compare_order = compare_order
109 self.compare_fill = compare_fill
110 self.sequences = {}
111 if sequences:
112 for key, value in sequences.items():
113 self.sequences[key.lower()] = value
115 def parse(self, version_str):
116 """
117 Parse the version using this scheme from the given string. Returns None if unable to parse.
119 :param version_str: A string that may contain a version in this version scheme.
120 :returns: the parts of the version identified with the regular expression or None.
121 :rtype: list of str or None
122 """
123 match = re.match(self.parse_regex, version_str, flags=self.parse_flags)
124 result = None
125 if match:
126 result = []
127 for item in match.groups():
128 if item is None:
129 item = self.clear_value
130 result.append(item)
131 return result
133 #############################################################
134 # The rest of these are used by unit test for regex changes
136 def _is_match(self, version_str):
137 """
138 Is this versioning scheme able to successfully parse the given string?
140 :param version_str: a string containing a version
141 :type version_str: str
142 :return: asserted if able to parse the given version string
143 :rtype: bool
144 """
145 match = re.match(self.parse_regex, version_str, flags=self.parse_flags)
146 return not (not match)
148 def _release(self, version_str):
149 """
150 Get the first matching group of the version.
152 :param version_str: a string containing a version
153 :type version_str: str
154 :return: the first matching group of the version
155 :rtype: str or None
156 """
157 result = None
158 match = re.match(self.parse_regex, version_str, flags=self.parse_flags)
159 if match:
160 result = match.group(1)
161 return result
163 def _pre(self, version_str):
164 """
166 :param version_str: a string containing a version
167 :type version_str: str
168 :return: the second matching group of the version
169 :rtype: str or None
170 """
171 result = None
172 match = re.match(self.parse_regex, version_str, flags=self.parse_flags)
173 if match:
174 result = match.group(2)
175 return result
177 def _post(self, version_str):
178 """
180 :param version_str: a string containing a version
181 :type version_str: str
182 :return: the third matching group of the version
183 :rtype: str or None
184 """
185 result = None
186 match = re.match(self.parse_regex, version_str, flags=self.parse_flags)
187 if match:
188 result = match.group(3)
189 return result
191 def _dev(self, version_str):
192 """
194 :param version_str: a string containing a version
195 :type version_str: str
196 :return: the fourth matching group of the version
197 :rtype: str or None
198 """
199 result = None
200 match = re.match(self.parse_regex, version_str, flags=self.parse_flags)
201 if match:
202 result = match.group(4)
203 return result
205 def _local(self, version_str):
206 """
208 :param version_str: a string containing a version
209 :type version_str: str
210 :return: the fifth matching group of the version
211 :rtype: str|int|None
212 """
213 result = None
214 match = re.match(self.parse_regex, version_str, flags=self.parse_flags)
215 if match:
216 result = match.group(5)
217 return result
220class VersionSplitScheme(AVersionScheme):
221 """
222 Support splitting a version string into a variable number of segments.
224 For example, "1.2.3" => ['1', '2', '3']
226 When comparing versions, right pad with clear_value the segments until both versions have
227 the same number of segments, then perform the compare.
228 """
229 def __init__(self, name, split_regex=r"\.", clear_value='0', join_str='.', description=None):
230 """
231 :param name: the name of the versioning scheme.
232 :type name: str
233 :param split_regex: the regular expression that splits the version into sequences.
234 :type split_regex: str
235 :param clear_value: the value that the fields to the right of the bumped field get set to.
236 :type clear_value: str
237 :param join_str: the sequence separator string
238 :type join_str: str
239 :param description: the description of the versioning scheme
240 :type description: str
241 """
242 super(VersionSplitScheme, self).__init__(name=name, description=description)
243 self.split_regex = split_regex
244 self.clear_value = clear_value
245 self.join_str = join_str
247 def parse(self, version_str):
248 """
249 Parse the version using this scheme from the given string. Returns None if unable to parse.
251 :param version_str: A string that may contain a version in this version scheme.
252 :returns: the parts of the version identified with the regular expression or None.
253 :rtype: list of str or None
254 """
255 parts = re.split(self.split_regex, version_str)
256 if not parts[-1]:
257 raise AttributeError('Version can not end in a version separator')
258 return parts
260 #############################################################
261 # The rest of these are used by unit test for regex changes
263 def _is_match(self, version_str):
264 """
265 Is this versioning scheme able to successfully parse the given string?
267 :param version_str: a string containing a version
268 :type version_str: str
269 :return: asserted if able to parse the given version string
270 :rtype: bool
271 """
272 # noinspection PyBroadException
273 try:
274 self.parse(version_str)
275 return True
276 except Exception:
277 return False
279 def _release(self, version_str):
280 """
281 Get the first matching group of the version.
283 :param version_str: a string containing a version
284 :type version_str: str
285 :return: the first matching group of the version
286 :rtype: str or None
287 """
288 result = None
289 parts = self.parse(version_str)
290 if parts:
291 result = parts[0]
292 return result
294# now define the supported version schemes:
297Simple3VersionScheme = VersionScheme(name="A.B.C",
298 parse_regex=r"^(\d+)\.(\d+)\.(\d+)$",
299 clear_value='0',
300 format_str="{0}.{1}.{2}",
301 fields=['Major', 'Minor', 'Tiny'],
302 description='Simple Major.Minor.Tiny version scheme')
304Simple4VersionScheme = VersionScheme(name="A.B.C.D",
305 parse_regex=r"^(\d+)\.(\d+)\.(\d+)\.(\d+)$",
306 clear_value='0',
307 format_str="{0}.{1}.{2}.{3}",
308 fields=['Major', 'Minor', 'Tiny', 'Tiny2'],
309 description='Simple Major.Minor.Tiny.Tiny2 version scheme')
311Simple5VersionScheme = VersionScheme(name="A.B.C.D.E",
312 parse_regex=r"^(\d+)\.(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?$",
313 clear_value='0',
314 format_str="{0}.{1}.{2}.{3}.{4}",
315 format_types=[int, int, int, int, int],
316 fields=['Major', 'Minor', 'Tiny', 'Build', 'Patch'],
317 description='Simple Major.Minor.Tiny.Build.Patch version scheme')
319VariableDottedIntegerVersionScheme = VersionSplitScheme(name='A.B...',
320 description='A variable number of dot separated '
321 'integers version scheme')
323Pep440VersionScheme = VersionScheme(name="pep440",
324 parse_regex=r"""
325 ^
326 (\d[\.\d]*(?<= \d))
327 ((?:[abc]|rc)\d+)?
328 (?:(\.post\d+))?
329 (?:(\.dev\d+))?
330 (?:(\+(?![.])[a-zA-Z0-9\.]*[a-zA-Z0-9]))?
331 $
332 """,
333 compare_order=[0, 1, 2, 3, 4],
334 compare_fill=['~', '~', '', '~', ''],
335 parse_flags=re.VERBOSE,
336 clear_value=None,
337 format_str='{0}{1}{2}{3}{4}',
338 fields=['Release', 'Pre', 'Post', 'Dev', 'Local'],
339 subfields={'Release': ['Major', 'Minor', 'Tiny', 'Tiny2']},
340 sequences={'Pre': ['a', 'b', 'c', 'rc'],
341 'Post': ['.post'],
342 'Dev': ['.dev'],
343 'Local': ['+']},
344 description=dedent("""\
345 PEP 440
346 Public version identifiers MUST comply with the following scheme:
348 N[.N]+[{a|b|c|rc}N][.postN][.devN][+local]
350 Public version identifiers MUST NOT include leading or trailing whitespace.
352 Public version identifiers MUST be unique within a given distribution.
354 Public version identifiers are separated into up to five segments:
356 Release segment: N[.N]+
357 Pre-release segment: {a|b|c|rc}N
358 Post-release segment: .postN
359 Development release segment: .devN
360 Local release segment: +local
362 The local version labels MUST be limited to the following set of permitted
363 characters:
365 ASCII letters ( [a-zA-Z] )
366 ASCII digits ( [0-9] )
367 periods ( . )
369 Local version labels MUST start and end with an ASCII letter or digit.
370 """))
372PerlVersionScheme = VersionScheme(name="A.B",
373 parse_regex=r"^(\d+)\.(\d+)$",
374 clear_value='0',
375 format_str="{0:d}.{1:02d}",
376 format_types=[int, int],
377 fields=['Major', 'Minor'],
378 description='perl Major.Minor version scheme')