Coverage for plugins/module_utils/version.py: 43%

109 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-30 17:06 -0400

1# Vendored copy of distutils/version.py from CPython 3.9.5 

2# 

3# Implements multiple version numbering conventions for the 

4# Python Module Distribution Utilities. 

5# 

6# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0) 

7# 

8 

9"""Provides classes to represent module version numbers (one class for 

10each style of version numbering). There are currently two such classes 

11implemented: StrictVersion and LooseVersion. 

12 

13Every version number class implements the following interface: 

14 * the 'parse' method takes a string and parses it to some internal 

15 representation; if the string is an invalid version number, 

16 'parse' raises a ValueError exception 

17 * the class constructor takes an optional string argument which, 

18 if supplied, is passed to 'parse' 

19 * __str__ reconstructs the string that was passed to 'parse' (or 

20 an equivalent string -- ie. one that will generate an equivalent 

21 version number instance) 

22 * __repr__ generates Python code to recreate the version number instance 

23 * _cmp compares the current instance with either another instance 

24 of the same class or a string (which will be parsed to an instance 

25 of the same class, thus must follow the same rules) 

26""" 

27 

28from __future__ import absolute_import, division, print_function 

29 

30__metaclass__ = type 

31 

32import re 

33 

34try: 

35 RE_FLAGS = re.VERBOSE | re.ASCII 

36except AttributeError: 

37 RE_FLAGS = re.VERBOSE 

38 

39 

40class Version: 

41 """Abstract base class for version numbering classes. Just provides 

42 constructor (__init__) and reproducer (__repr__), because those 

43 seem to be the same for all version numbering classes; and route 

44 rich comparisons to _cmp. 

45 """ 

46 

47 def __init__(self, vstring=None): 

48 if vstring: 

49 self.parse(vstring) 

50 

51 def __repr__(self): 

52 return "%s ('%s')" % (self.__class__.__name__, str(self)) 

53 

54 def __eq__(self, other): 

55 c = self._cmp(other) 

56 if c is NotImplemented: 

57 return c 

58 return c == 0 

59 

60 def __lt__(self, other): 

61 c = self._cmp(other) 

62 if c is NotImplemented: 

63 return c 

64 return c < 0 

65 

66 def __le__(self, other): 

67 c = self._cmp(other) 

68 if c is NotImplemented: 

69 return c 

70 return c <= 0 

71 

72 def __gt__(self, other): 

73 c = self._cmp(other) 

74 if c is NotImplemented: 

75 return c 

76 return c > 0 

77 

78 def __ge__(self, other): 

79 c = self._cmp(other) 

80 if c is NotImplemented: 

81 return c 

82 return c >= 0 

83 

84 

85# Interface for version-number classes -- must be implemented 

86# by the following classes (the concrete ones -- Version should 

87# be treated as an abstract class). 

88# __init__ (string) - create and take same action as 'parse' 

89# (string parameter is optional) 

90# parse (string) - convert a string representation to whatever 

91# internal representation is appropriate for 

92# this style of version numbering 

93# __str__ (self) - convert back to a string; should be very similar 

94# (if not identical to) the string supplied to parse 

95# __repr__ (self) - generate Python code to recreate 

96# the instance 

97# _cmp (self, other) - compare two version numbers ('other' may 

98# be an unparsed version string, or another 

99# instance of your version class) 

100 

101 

102class StrictVersion(Version): 

103 """Version numbering for anal retentives and software idealists. 

104 Implements the standard interface for version number classes as 

105 described above. A version number consists of two or three 

106 dot-separated numeric components, with an optional "pre-release" tag 

107 on the end. The pre-release tag consists of the letter 'a' or 'b' 

108 followed by a number. If the numeric components of two version 

109 numbers are equal, then one with a pre-release tag will always 

110 be deemed earlier (lesser) than one without. 

111 

112 The following are valid version numbers (shown in the order that 

113 would be obtained by sorting according to the supplied cmp function): 

114 

115 0.4 0.4.0 (these two are equivalent) 

116 0.4.1 

117 0.5a1 

118 0.5b3 

119 0.5 

120 0.9.6 

121 1.0 

122 1.0.4a3 

123 1.0.4b1 

124 1.0.4 

125 

126 The following are examples of invalid version numbers: 

127 

128 1 

129 2.7.2.2 

130 1.3.a4 

131 1.3pl1 

132 1.3c4 

133 

134 The rationale for this version numbering system will be explained 

135 in the distutils documentation. 

136 """ 

137 

138 version_re = re.compile(r"^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$", RE_FLAGS) 

139 

140 def parse(self, vstring): 

141 match = self.version_re.match(vstring) 

142 if not match: 

143 raise ValueError("invalid version number '%s'" % vstring) 

144 

145 major, minor, patch, prerelease, prerelease_num = match.group(1, 2, 4, 5, 6) 

146 

147 if patch: 

148 self.version = tuple(map(int, [major, minor, patch])) 

149 else: 

150 self.version = tuple(map(int, [major, minor])) + (0,) 

151 

152 if prerelease: 

153 self.prerelease = (prerelease[0], int(prerelease_num)) 

154 else: 

155 self.prerelease = None 

156 

157 def __str__(self): 

158 if self.version[2] == 0: 

159 vstring = ".".join(map(str, self.version[0:2])) 

160 else: 

161 vstring = ".".join(map(str, self.version)) 

162 

163 if self.prerelease: 

164 vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) 

165 

166 return vstring 

167 

168 def _cmp(self, other): 

169 if isinstance(other, str): 

170 other = StrictVersion(other) 

171 elif not isinstance(other, StrictVersion): 

172 return NotImplemented 

173 

174 if self.version != other.version: 

175 # numeric versions don't match 

176 # prerelease stuff doesn't matter 

177 if self.version < other.version: 

178 return -1 

179 else: 

180 return 1 

181 

182 # have to compare prerelease 

183 # case 1: neither has prerelease; they're equal 

184 # case 2: self has prerelease, other doesn't; other is greater 

185 # case 3: self doesn't have prerelease, other does: self is greater 

186 # case 4: both have prerelease: must compare them! 

187 

188 if not self.prerelease and not other.prerelease: 

189 return 0 

190 elif self.prerelease and not other.prerelease: 

191 return -1 

192 elif not self.prerelease and other.prerelease: 

193 return 1 

194 elif self.prerelease and other.prerelease: 

195 if self.prerelease == other.prerelease: 

196 return 0 

197 elif self.prerelease < other.prerelease: 

198 return -1 

199 else: 

200 return 1 

201 else: 

202 raise AssertionError("never get here") 

203 

204 

205# end class StrictVersion 

206 

207# The rules according to Greg Stein: 

208# 1) a version number has 1 or more numbers separated by a period or by 

209# sequences of letters. If only periods, then these are compared 

210# left-to-right to determine an ordering. 

211# 2) sequences of letters are part of the tuple for comparison and are 

212# compared lexicographically 

213# 3) recognize the numeric components may have leading zeroes 

214# 

215# The LooseVersion class below implements these rules: a version number 

216# string is split up into a tuple of integer and string components, and 

217# comparison is a simple tuple comparison. This means that version 

218# numbers behave in a predictable and obvious way, but a way that might 

219# not necessarily be how people *want* version numbers to behave. There 

220# wouldn't be a problem if people could stick to purely numeric version 

221# numbers: just split on period and compare the numbers as tuples. 

222# However, people insist on putting letters into their version numbers; 

223# the most common purpose seems to be: 

224# - indicating a "pre-release" version 

225# ('alpha', 'beta', 'a', 'b', 'pre', 'p') 

226# - indicating a post-release patch ('p', 'pl', 'patch') 

227# but of course this can't cover all version number schemes, and there's 

228# no way to know what a programmer means without asking him. 

229# 

230# The problem is what to do with letters (and other non-numeric 

231# characters) in a version number. The current implementation does the 

232# obvious and predictable thing: keep them as strings and compare 

233# lexically within a tuple comparison. This has the desired effect if 

234# an appended letter sequence implies something "post-release": 

235# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". 

236# 

237# However, if letters in a version number imply a pre-release version, 

238# the "obvious" thing isn't correct. Eg. you would expect that 

239# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison 

240# implemented here, this just isn't so. 

241# 

242# Two possible solutions come to mind. The first is to tie the 

243# comparison algorithm to a particular set of semantic rules, as has 

244# been done in the StrictVersion class above. This works great as long 

245# as everyone can go along with bondage and discipline. Hopefully a 

246# (large) subset of Python module programmers will agree that the 

247# particular flavour of bondage and discipline provided by StrictVersion 

248# provides enough benefit to be worth using, and will submit their 

249# version numbering scheme to its domination. The free-thinking 

250# anarchists in the lot will never give in, though, and something needs 

251# to be done to accommodate them. 

252# 

253# Perhaps a "moderately strict" version class could be implemented that 

254# lets almost anything slide (syntactically), and makes some heuristic 

255# assumptions about non-digits in version number strings. This could 

256# sink into special-case-hell, though; if I was as talented and 

257# idiosyncratic as Larry Wall, I'd go ahead and implement a class that 

258# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is 

259# just as happy dealing with things like "2g6" and "1.13++". I don't 

260# think I'm smart enough to do it right though. 

261# 

262# In any case, I've coded the test suite for this module (see 

263# ../test/test_version.py) specifically to fail on things like comparing 

264# "1.2a2" and "1.2". That's not because the *code* is doing anything 

265# wrong, it's because the simple, obvious design doesn't match my 

266# complicated, hairy expectations for real-world version numbers. It 

267# would be a snap to fix the test suite to say, "Yep, LooseVersion does 

268# the Right Thing" (ie. the code matches the conception). But I'd rather 

269# have a conception that matches common notions about version numbers. 

270 

271 

272class LooseVersion(Version): 

273 """Version numbering for anarchists and software realists. 

274 Implements the standard interface for version number classes as 

275 described above. A version number consists of a series of numbers, 

276 separated by either periods or strings of letters. When comparing 

277 version numbers, the numeric components will be compared 

278 numerically, and the alphabetic components lexically. The following 

279 are all valid version numbers, in no particular order: 

280 

281 1.5.1 

282 1.5.2b2 

283 161 

284 3.10a 

285 8.02 

286 3.4j 

287 1996.07.12 

288 3.2.pl0 

289 3.1.1.6 

290 2g6 

291 11g 

292 0.960923 

293 2.2beta29 

294 1.13++ 

295 5.5.kw 

296 2.0b1pl0 

297 

298 In fact, there is no such thing as an invalid version number under 

299 this scheme; the rules for comparison are simple and predictable, 

300 but may not always give the results you want (for some definition 

301 of "want"). 

302 """ 

303 

304 component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE) 

305 

306 def __init__(self, vstring=None): 

307 if vstring: 

308 self.parse(vstring) 

309 

310 def parse(self, vstring): 

311 # I've given up on thinking I can reconstruct the version string 

312 # from the parsed tuple -- so I just store the string here for 

313 # use by __str__ 

314 self.vstring = vstring 

315 components = [x for x in self.component_re.split(vstring) if x and x != "."] 

316 for i, obj in enumerate(components): 

317 try: 

318 components[i] = int(obj) 

319 except ValueError: 

320 pass 

321 

322 self.version = components 

323 

324 def __str__(self): 

325 return self.vstring 

326 

327 def __repr__(self): 

328 return "LooseVersion ('%s')" % str(self) 

329 

330 def _cmp(self, other): 

331 if isinstance(other, str): 

332 other = LooseVersion(other) 

333 elif not isinstance(other, LooseVersion): 

334 return NotImplemented 

335 

336 if self.version == other.version: 

337 return 0 

338 if self.version < other.version: 

339 return -1 

340 if self.version > other.version: 

341 return 1 

342 

343 

344# end class LooseVersion