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

3A generic version class that supports comparison and version bumping (incrementing by part, for example you may 

4bump the minor part of '1.2.3' yielding '1.3.0'). 

5 

6Four version schemes are included:: 

7 

8 * Simple3VersionScheme which supports 3 numerical part versions (A.B.C where A, B, and C are integers) 

9 * Simple4VersionScheme which supports 4 numerical part versions (A.B.C.D where A, B, C, and D are integers) 

10 * Pep440VersionScheme which supports PEP 440 (http://www.python.org/dev/peps/pep-0440/) versions 

11 (N[.N]+[{a|b|c|rc}N][.postN][.devN]) 

12 * PerlVersionScheme which supports 2 numerical part versions where the second part is at least two digits 

13 (A.BB where A and B are integers and B is zero padded on the left. For example: 1.02, 1.34, 1.567) 

14 

15If you don't specify which version scheme the version instance uses, then it will use the first scheme from the 

16SupportedVersionSchemes list that successfully parses the version string 

17 

18By default, Pep440VersionScheme is the supported scheme. To change to a different list of schemes, use the 

19**Version.set_supported_version_schemes(schemes)**. For example:: 

20 

21 from versio.version_scheme import Simple3VersionScheme, PerlVersionScheme 

22 Version.set_supported_version_schemes([Simple3VersionScheme, PerlVersionScheme]) 

23 

24In addition, you may define your own version scheme by extending VersionScheme. 

25""" 

26 

27__docformat__ = "restructuredtext en" 

28 

29import re 

30from versio.comparable_mixin import ComparableMixin 

31from versio.version_scheme import Pep440VersionScheme, VersionScheme 

32 

33 

34class Version(ComparableMixin): 

35 """ 

36 A version class that supports multiple versioning schemes, version comparisons, 

37 and version bumping. 

38 """ 

39 

40 # Version uses the SupportedVersionSchemes list when trying to parse a string where 

41 # the given scheme is None. The parsing attempts will be sequentially thru this list 

42 # until a match is found. 

43 supported_version_schemes = [ 

44 # Simple3VersionScheme, 

45 # Simple4VersionScheme, 

46 Pep440VersionScheme, 

47 ] 

48 

49 def _cmpkey(self, other=None): 

50 """ 

51 A key for comparisons required by ComparableMixin 

52 

53 Here we just use the list of version parts. 

54 """ 

55 parts = self.parts[:] 

56 if self.compare_order: 

57 for index, value in enumerate(self.compare_order): 

58 parts[index] = self.parts[value] 

59 # if self.parts_reverse: 

60 # parts.reverse() 

61 key = [] 

62 for index, part in enumerate(parts): 

63 if part is None: 

64 if self.compare_fill is None: 

65 key.append("~") 

66 else: 

67 key.append(self.compare_fill[index]) 

68 else: 

69 sub_parts = part.split(".") 

70 for sub_part in sub_parts: 

71 if sub_part: 

72 try: 

73 key.append(int(sub_part)) 

74 except ValueError: 

75 key.append(sub_part) 

76 

77 try: 

78 if other is not None: 

79 other_part = other.parts[index] 

80 if other_part is not None: 

81 extra_sequences = len(other_part.split(".")) - len( 

82 sub_parts 

83 ) 

84 if extra_sequences > 0: 

85 try: 

86 key += [ 

87 int(self.scheme.extend_value) 

88 ] * extra_sequences 

89 except ValueError: 

90 key += [self.scheme.extend_value] * extra_sequences 

91 except IndexError as ex: 

92 print(str(ex)) 

93 

94 return key 

95 

96 def _compare(self, other, method): 

97 """ 

98 Compare an object with this object using the given comparison method. 

99 

100 :param other: object ot compare with 

101 :type other: ComparableMixin 

102 :param method: a comparison method 

103 :type method: lambda 

104 :return: asserted if comparison is true 

105 :rtype: bool 

106 :raises: NotImplemented 

107 """ 

108 

109 if not isinstance(other, Version): 

110 try: 

111 other = Version(str(other), scheme=self.scheme) 

112 except AttributeError: 

113 return NotImplemented 

114 

115 try: 

116 x_cmpkey = self._cmpkey(other)[:] 

117 y_cmpkey = other._cmpkey(self)[:] 

118 # make same length 

119 x_cmpkey += [self.scheme.clear_value] * (len(y_cmpkey) - len(x_cmpkey)) 

120 y_cmpkey += [self.scheme.clear_value] * (len(x_cmpkey) - len(y_cmpkey)) 

121 

122 for index, x in enumerate(x_cmpkey): 

123 y = y_cmpkey[index] 

124 try: 

125 if int(x) == int(y): 

126 continue 

127 except (TypeError, ValueError): 

128 if str(x) == str(y): 

129 continue 

130 

131 try: 

132 if method(int(x), int(y)): 

133 return True 

134 except (TypeError, ValueError): 

135 if method(str(x), str(y)): 

136 return True 

137 return False 

138 

139 x0 = x_cmpkey[0] 

140 y0 = y_cmpkey[0] 

141 

142 try: 

143 if method(int(x0), int(y0)): 

144 return True 

145 except (TypeError, ValueError): 

146 if method(str(x0), str(y0)): 

147 return True 

148 return False 

149 except (AttributeError, TypeError): 

150 # _cmpkey not implemented, or return different type, 

151 # so I can't compare with "other". 

152 return NotImplemented 

153 

154 @classmethod 

155 def set_supported_version_schemes(cls, schemes): 

156 """ 

157 Set the list of version schemes used when parsing a string. 

158 

159 :param schemes: list of version schemes. 

160 """ 

161 cls.supported_version_schemes = list(schemes) 

162 

163 def __init__(self, version_str=None, scheme=None): 

164 """ 

165 Creates a version instance which is bound to a version scheme. 

166 

167 :param version_str: the initial version as a string 

168 :type version_str: str or None 

169 :param scheme: the version scheme to use to parse the version string or None to try all supported 

170 version schemes. 

171 :type scheme: VersionScheme or None 

172 """ 

173 self.scheme, self.parts = self._parse(version_str, scheme) 

174 

175 if not self.scheme: 

176 raise AttributeError( 

177 'Can not find supported scheme for "{ver}"'.format(ver=version_str) 

178 ) 

179 

180 if not self.parts: 

181 raise AttributeError('Can not parse "{ver}"'.format(ver=version_str)) 

182 

183 self.compare_order = self.scheme.compare_order 

184 self.compare_fill = self.scheme.compare_fill 

185 

186 def _parse(self, version_str, scheme): 

187 """ 

188 Parse the given version string using the given version scheme. If the given version scheme 

189 is None, then try all supported version schemes stopping with the first one able to successfully 

190 parse the version string. 

191 

192 :param version_str: the version string to parse 

193 :type version_str: str 

194 :param scheme: the version scheme to use 

195 :type scheme: VersionScheme or None 

196 :return: the version scheme that can parse the version string, and the version parts as parsed. 

197 :rtype: VersionScheme or None, list 

198 """ 

199 if scheme is None: 

200 for trial_scheme in self.supported_version_schemes: 

201 parts = self._parse_with_scheme(version_str, trial_scheme) 

202 if parts: 

203 return trial_scheme, parts 

204 else: 

205 return scheme, self._parse_with_scheme(version_str, scheme) 

206 return None 

207 

208 # noinspection PyMethodMayBeStatic 

209 def _parse_with_scheme(self, version_str, scheme): 

210 """ 

211 Parse the version string with the given version scheme. 

212 

213 :param version_str: the version string to parse 

214 :type version_str: str or None 

215 :param scheme: the version scheme to use 

216 :type scheme: VersionScheme 

217 :returns the parts of the version identified with the regular expression or None. 

218 :rtype: list of str or None 

219 """ 

220 if version_str is None: 

221 return False 

222 return scheme.parse(version_str) 

223 

224 def __str__(self): 

225 """ 

226 Render to version to a string. 

227 

228 :return: the version as a string. 

229 :rtype: str 

230 """ 

231 if self.parts: 

232 # for variable dotted scheme 

233 if getattr(self.scheme, "join_str", None) is not None: 

234 return self.scheme.join_str.join(self.parts) 

235 

236 # for other schemes 

237 casts = self.scheme.format_types 

238 casts += [str] * ( 

239 len(self.scheme.fields) - len(casts) 

240 ) # right fill with str types 

241 

242 parts = [part or "" for part in self.parts] 

243 parts += [""] * ( 

244 len(self.scheme.fields) - len(parts) 

245 ) # right fill with blanks 

246 

247 def _type_cast(value, cast): 

248 """cast the given value to the given cast or str if cast is None""" 

249 if cast is None: 

250 cast = str 

251 result = None 

252 try: 

253 result = cast(value) 

254 except ValueError: 

255 pass 

256 return result 

257 

258 args = list(map(_type_cast, parts, casts)) 

259 return self.scheme.format_str.format(*args) 

260 return "Unknown version" 

261 

262 def bump(self, field_name=None, sub_index=-1, sequence=-1, promote=False): 

263 """ 

264 Bump the given version field by 1. If no field name is given, 

265 then bump the least significant field. 

266 

267 Optionally can bump by sequence index where 0 is the left most part of the version. 

268 If a field_name is given, then the sequence value will be ignored. 

269 

270 :param field_name: the field name that matches one of the scheme's fields 

271 :type field_name: object 

272 :param sub_index: index in field 

273 :type sub_index: int 

274 :param sequence: the zero offset sequence index to bump. 

275 :type sequence: int 

276 :param promote: assert if end of field sequence causes field to be cleared 

277 :type promote: bool 

278 :return: True on success 

279 :rtype: bool 

280 """ 

281 if field_name is None: 

282 if sequence >= 0: 

283 # noinspection PyUnusedLocal 

284 for idx in range(len(self.parts) - 1, sequence): 

285 self.parts.append(self.scheme.clear_value) 

286 self.parts[sequence] = str(int(self.parts[sequence]) + 1) 

287 for idx in range(sequence + 1, len(self.parts)): 

288 self.parts[idx] = self.scheme.clear_value 

289 return True 

290 if getattr(self.scheme, "fields", None) is None: 

291 self.parts[-1] = str(int(self.parts[-1]) + 1) 

292 return True 

293 field_name = self.scheme.fields[-1] 

294 field_name = field_name.lower() 

295 

296 index = 0 

297 # noinspection PyBroadException 

298 try: 

299 bumped = False 

300 index = self.scheme.fields.index(field_name) 

301 for idx, part in enumerate(self.parts): 

302 if idx == index: 

303 bumped_part = self._bump_parse(field_name, part, sub_index) 

304 if self.parts[idx] != bumped_part: 

305 self.parts[idx] = bumped_part 

306 bumped = True 

307 

308 if idx > index: 

309 self.parts[idx] = self.scheme.clear_value 

310 return bumped 

311 except (IndexError, ValueError): 

312 # not if fields, try subfields 

313 if field_name in self.scheme.subfields: 

314 return self.bump(*self.scheme.subfields[field_name]) 

315 if promote: 

316 self.parts[index] = self.scheme.clear_value 

317 return True 

318 return False 

319 

320 def _increment(self, field_name, value): 

321 """ 

322 Increment the value for the given field. 

323 

324 :param field_name: the field we are incrementing 

325 :type field_name: str 

326 :param value: the field's value 

327 :type value: int or str 

328 :return: the value after incrementing 

329 :rtype: int or str 

330 """ 

331 if isinstance(value, int): 

332 value += 1 

333 if isinstance(value, str): 

334 if field_name in self.scheme.sequences: 

335 seq_list = self.scheme.sequences[field_name] 

336 if not value: 

337 return seq_list[0] 

338 if value not in seq_list: 

339 raise AttributeError( 

340 "Can not bump version, the current value (%s) not in sequence constraints" 

341 % value 

342 ) 

343 idx = seq_list.index(value) + 1 

344 if idx < len(seq_list): 

345 return seq_list[idx] 

346 else: 

347 raise IndexError("Can not increment past end of sequence") 

348 else: 

349 value = chr(ord(value) + 1) 

350 return value 

351 

352 def _part_increment(self, field_name, sub_index, separator, sub_parts, clear_value): 

353 """ 

354 Increment a version part, including handing parts to the right of the field being incremented. 

355 

356 :param field_name: the field we are incrementing 

357 :type field_name: str 

358 :param sub_index: the index of the sub part we are incrementing 

359 :type sub_index: int 

360 :param separator: the separator between sub parts 

361 :type separator: str|None 

362 :param sub_parts: the sub parts of a version part 

363 :type sub_parts: list[int|str] 

364 :param clear_value: the value to set parts to the right of this part to after incrementing. 

365 :type clear_value: str|None 

366 :return: 

367 :rtype: 

368 """ 

369 sub_parts[sub_index] = self._increment(field_name, sub_parts[sub_index]) 

370 if sub_index >= 0: 

371 for sub_idx in range(sub_index + 1, len(sub_parts)): 

372 sub_parts[sub_idx] = clear_value 

373 return separator.join([str(n) for n in sub_parts]) 

374 

375 def _bump_parse(self, field_name, part, sub_index): 

376 """ 

377 Bump (increment) the given field of a version. 

378 

379 :param field_name: the field we are incrementing 

380 :type field_name: str 

381 :param part: the version part being incremented 

382 :type part: str or None 

383 :param sub_index: 

384 :type sub_index: 

385 :return: the version part after incrementing 

386 :rtype: int or str or None 

387 """ 

388 if part is None: 

389 value = self.scheme.clear_value or "1" 

390 return "{seq}{value}".format( 

391 seq=self.scheme.sequences[field_name][0], value=value 

392 ) 

393 

394 # noinspection RegExpRedundantEscape 

395 match = re.match(r"^\d[\.\d]*(?<=\d)$", part) 

396 if match: 

397 # dotted numeric (ex: '1.2.3') 

398 return self._part_increment( 

399 field_name, 

400 sub_index, 

401 ".", 

402 [int(n) for n in part.split(".")], 

403 self.scheme.clear_value or "0", 

404 ) 

405 

406 match = re.match(r"(\.?[a-zA-Z+]*)(\d+)", part) 

407 if match: 

408 # alpha + numeric (ex: 'a1', 'rc2', '.post3') 

409 return self._part_increment( 

410 field_name, 

411 sub_index, 

412 "", 

413 [match.group(1) or "", int(match.group(2))], 

414 self.scheme.clear_value or "1", 

415 ) 

416 

417 return part