Hide keyboard shortcuts

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. 

4 

5 A version scheme consists of a:: 

6 

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. 

20 

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""" 

27 

28# noinspection PyUnusedName 

29__docformat__ = 'restructuredtext en' 

30 

31import re 

32from textwrap import dedent 

33 

34 

35class AVersionScheme(object): 

36 def __init__(self, name, description=None): 

37 """ 

38 The commonality between version schemes. 

39 

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' 

51 

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. 

56 

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 

62 

63 

64class VersionScheme(AVersionScheme): 

65 """Describe a versioning scheme""" 

66 

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] 

106 

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 

114 

115 def parse(self, version_str): 

116 """ 

117 Parse the version using this scheme from the given string. Returns None if unable to parse. 

118 

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 

132 

133 ############################################################# 

134 # The rest of these are used by unit test for regex changes 

135 

136 def _is_match(self, version_str): 

137 """ 

138 Is this versioning scheme able to successfully parse the given string? 

139 

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) 

147 

148 def _release(self, version_str): 

149 """ 

150 Get the first matching group of the version. 

151 

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 

162 

163 def _pre(self, version_str): 

164 """ 

165 

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 

176 

177 def _post(self, version_str): 

178 """ 

179 

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 

190 

191 def _dev(self, version_str): 

192 """ 

193 

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 

204 

205 def _local(self, version_str): 

206 """ 

207 

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 

218 

219 

220class VersionSplitScheme(AVersionScheme): 

221 """ 

222 Support splitting a version string into a variable number of segments. 

223 

224 For example, "1.2.3" => ['1', '2', '3'] 

225 

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 

246 

247 def parse(self, version_str): 

248 """ 

249 Parse the version using this scheme from the given string. Returns None if unable to parse. 

250 

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 

259 

260 ############################################################# 

261 # The rest of these are used by unit test for regex changes 

262 

263 def _is_match(self, version_str): 

264 """ 

265 Is this versioning scheme able to successfully parse the given string? 

266 

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 

278 

279 def _release(self, version_str): 

280 """ 

281 Get the first matching group of the version. 

282 

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 

293 

294# now define the supported version schemes: 

295 

296 

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') 

303 

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') 

310 

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') 

318 

319VariableDottedIntegerVersionScheme = VersionSplitScheme(name='A.B...', 

320 description='A variable number of dot separated ' 

321 'integers version scheme') 

322 

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: 

347 

348 N[.N]+[{a|b|c|rc}N][.postN][.devN][+local] 

349 

350 Public version identifiers MUST NOT include leading or trailing whitespace. 

351 

352 Public version identifiers MUST be unique within a given distribution. 

353 

354 Public version identifiers are separated into up to five segments: 

355 

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 

361 

362 The local version labels MUST be limited to the following set of permitted 

363 characters: 

364 

365 ASCII letters ( [a-zA-Z] ) 

366 ASCII digits ( [0-9] ) 

367 periods ( . ) 

368 

369 Local version labels MUST start and end with an ASCII letter or digit. 

370 """)) 

371 

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')