Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/packaging/version.py: 90%

163 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:25 +0000

1# This file is dual licensed under the terms of the Apache License, Version 

2# 2.0, and the BSD License. See the LICENSE file in the root of this repository 

3# for complete details. 

4""" 

5.. testsetup:: 

6 

7 from packaging.version import parse, Version 

8""" 

9 

10import collections 

11import itertools 

12import re 

13from typing import Any, Callable, Optional, SupportsInt, Tuple, Union 

14 

15from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType 

16 

17__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] 

18 

19InfiniteTypes = Union[InfinityType, NegativeInfinityType] 

20PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] 

21SubLocalType = Union[InfiniteTypes, int, str] 

22LocalType = Union[ 

23 NegativeInfinityType, 

24 Tuple[ 

25 Union[ 

26 SubLocalType, 

27 Tuple[SubLocalType, str], 

28 Tuple[NegativeInfinityType, SubLocalType], 

29 ], 

30 ..., 

31 ], 

32] 

33CmpKey = Tuple[ 

34 int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType 

35] 

36VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] 

37 

38_Version = collections.namedtuple( 

39 "_Version", ["epoch", "release", "dev", "pre", "post", "local"] 

40) 

41 

42 

43def parse(version: str) -> "Version": 

44 """Parse the given version string. 

45 

46 >>> parse('1.0.dev1') 

47 <Version('1.0.dev1')> 

48 

49 :param version: The version string to parse. 

50 :raises InvalidVersion: When the version string is not a valid version. 

51 """ 

52 return Version(version) 

53 

54 

55class InvalidVersion(ValueError): 

56 """Raised when a version string is not a valid version. 

57 

58 >>> Version("invalid") 

59 Traceback (most recent call last): 

60 ... 

61 packaging.version.InvalidVersion: Invalid version: 'invalid' 

62 """ 

63 

64 

65class _BaseVersion: 

66 _key: Tuple[Any, ...] 

67 

68 def __hash__(self) -> int: 

69 return hash(self._key) 

70 

71 # Please keep the duplicated `isinstance` check 

72 # in the six comparisons hereunder 

73 # unless you find a way to avoid adding overhead function calls. 

74 def __lt__(self, other: "_BaseVersion") -> bool: 

75 if not isinstance(other, _BaseVersion): 

76 return NotImplemented 

77 

78 return self._key < other._key 

79 

80 def __le__(self, other: "_BaseVersion") -> bool: 

81 if not isinstance(other, _BaseVersion): 

82 return NotImplemented 

83 

84 return self._key <= other._key 

85 

86 def __eq__(self, other: object) -> bool: 

87 if not isinstance(other, _BaseVersion): 

88 return NotImplemented 

89 

90 return self._key == other._key 

91 

92 def __ge__(self, other: "_BaseVersion") -> bool: 

93 if not isinstance(other, _BaseVersion): 

94 return NotImplemented 

95 

96 return self._key >= other._key 

97 

98 def __gt__(self, other: "_BaseVersion") -> bool: 

99 if not isinstance(other, _BaseVersion): 

100 return NotImplemented 

101 

102 return self._key > other._key 

103 

104 def __ne__(self, other: object) -> bool: 

105 if not isinstance(other, _BaseVersion): 

106 return NotImplemented 

107 

108 return self._key != other._key 

109 

110 

111# Deliberately not anchored to the start and end of the string, to make it 

112# easier for 3rd party code to reuse 

113_VERSION_PATTERN = r""" 

114 v? 

115 (?: 

116 (?:(?P<epoch>[0-9]+)!)? # epoch 

117 (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment 

118 (?P<pre> # pre-release 

119 [-_\.]? 

120 (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview)) 

121 [-_\.]? 

122 (?P<pre_n>[0-9]+)? 

123 )? 

124 (?P<post> # post release 

125 (?:-(?P<post_n1>[0-9]+)) 

126 | 

127 (?: 

128 [-_\.]? 

129 (?P<post_l>post|rev|r) 

130 [-_\.]? 

131 (?P<post_n2>[0-9]+)? 

132 ) 

133 )? 

134 (?P<dev> # dev release 

135 [-_\.]? 

136 (?P<dev_l>dev) 

137 [-_\.]? 

138 (?P<dev_n>[0-9]+)? 

139 )? 

140 ) 

141 (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version 

142""" 

143 

144VERSION_PATTERN = _VERSION_PATTERN 

145""" 

146A string containing the regular expression used to match a valid version. 

147 

148The pattern is not anchored at either end, and is intended for embedding in larger 

149expressions (for example, matching a version number as part of a file name). The 

150regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE`` 

151flags set. 

152 

153:meta hide-value: 

154""" 

155 

156 

157class Version(_BaseVersion): 

158 """This class abstracts handling of a project's versions. 

159 

160 A :class:`Version` instance is comparison aware and can be compared and 

161 sorted using the standard Python interfaces. 

162 

163 >>> v1 = Version("1.0a5") 

164 >>> v2 = Version("1.0") 

165 >>> v1 

166 <Version('1.0a5')> 

167 >>> v2 

168 <Version('1.0')> 

169 >>> v1 < v2 

170 True 

171 >>> v1 == v2 

172 False 

173 >>> v1 > v2 

174 False 

175 >>> v1 >= v2 

176 False 

177 >>> v1 <= v2 

178 True 

179 """ 

180 

181 _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) 

182 _key: CmpKey 

183 

184 def __init__(self, version: str) -> None: 

185 """Initialize a Version object. 

186 

187 :param version: 

188 The string representation of a version which will be parsed and normalized 

189 before use. 

190 :raises InvalidVersion: 

191 If the ``version`` does not conform to PEP 440 in any way then this 

192 exception will be raised. 

193 """ 

194 

195 # Validate the version and parse it into pieces 

196 match = self._regex.search(version) 

197 if not match: 

198 raise InvalidVersion(f"Invalid version: '{version}'") 

199 

200 # Store the parsed out pieces of the version 

201 self._version = _Version( 

202 epoch=int(match.group("epoch")) if match.group("epoch") else 0, 

203 release=tuple(int(i) for i in match.group("release").split(".")), 

204 pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), 

205 post=_parse_letter_version( 

206 match.group("post_l"), match.group("post_n1") or match.group("post_n2") 

207 ), 

208 dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), 

209 local=_parse_local_version(match.group("local")), 

210 ) 

211 

212 # Generate a key which will be used for sorting 

213 self._key = _cmpkey( 

214 self._version.epoch, 

215 self._version.release, 

216 self._version.pre, 

217 self._version.post, 

218 self._version.dev, 

219 self._version.local, 

220 ) 

221 

222 def __repr__(self) -> str: 

223 """A representation of the Version that shows all internal state. 

224 

225 >>> Version('1.0.0') 

226 <Version('1.0.0')> 

227 """ 

228 return f"<Version('{self}')>" 

229 

230 def __str__(self) -> str: 

231 """A string representation of the version that can be rounded-tripped. 

232 

233 >>> str(Version("1.0a5")) 

234 '1.0a5' 

235 """ 

236 parts = [] 

237 

238 # Epoch 

239 if self.epoch != 0: 

240 parts.append(f"{self.epoch}!") 

241 

242 # Release segment 

243 parts.append(".".join(str(x) for x in self.release)) 

244 

245 # Pre-release 

246 if self.pre is not None: 

247 parts.append("".join(str(x) for x in self.pre)) 

248 

249 # Post-release 

250 if self.post is not None: 

251 parts.append(f".post{self.post}") 

252 

253 # Development release 

254 if self.dev is not None: 

255 parts.append(f".dev{self.dev}") 

256 

257 # Local version segment 

258 if self.local is not None: 

259 parts.append(f"+{self.local}") 

260 

261 return "".join(parts) 

262 

263 @property 

264 def epoch(self) -> int: 

265 """The epoch of the version. 

266 

267 >>> Version("2.0.0").epoch 

268 0 

269 >>> Version("1!2.0.0").epoch 

270 1 

271 """ 

272 _epoch: int = self._version.epoch 

273 return _epoch 

274 

275 @property 

276 def release(self) -> Tuple[int, ...]: 

277 """The components of the "release" segment of the version. 

278 

279 >>> Version("1.2.3").release 

280 (1, 2, 3) 

281 >>> Version("2.0.0").release 

282 (2, 0, 0) 

283 >>> Version("1!2.0.0.post0").release 

284 (2, 0, 0) 

285 

286 Includes trailing zeroes but not the epoch or any pre-release / development / 

287 post-release suffixes. 

288 """ 

289 _release: Tuple[int, ...] = self._version.release 

290 return _release 

291 

292 @property 

293 def pre(self) -> Optional[Tuple[str, int]]: 

294 """The pre-release segment of the version. 

295 

296 >>> print(Version("1.2.3").pre) 

297 None 

298 >>> Version("1.2.3a1").pre 

299 ('a', 1) 

300 >>> Version("1.2.3b1").pre 

301 ('b', 1) 

302 >>> Version("1.2.3rc1").pre 

303 ('rc', 1) 

304 """ 

305 _pre: Optional[Tuple[str, int]] = self._version.pre 

306 return _pre 

307 

308 @property 

309 def post(self) -> Optional[int]: 

310 """The post-release number of the version. 

311 

312 >>> print(Version("1.2.3").post) 

313 None 

314 >>> Version("1.2.3.post1").post 

315 1 

316 """ 

317 return self._version.post[1] if self._version.post else None 

318 

319 @property 

320 def dev(self) -> Optional[int]: 

321 """The development number of the version. 

322 

323 >>> print(Version("1.2.3").dev) 

324 None 

325 >>> Version("1.2.3.dev1").dev 

326 1 

327 """ 

328 return self._version.dev[1] if self._version.dev else None 

329 

330 @property 

331 def local(self) -> Optional[str]: 

332 """The local version segment of the version. 

333 

334 >>> print(Version("1.2.3").local) 

335 None 

336 >>> Version("1.2.3+abc").local 

337 'abc' 

338 """ 

339 if self._version.local: 

340 return ".".join(str(x) for x in self._version.local) 

341 else: 

342 return None 

343 

344 @property 

345 def public(self) -> str: 

346 """The public portion of the version. 

347 

348 >>> Version("1.2.3").public 

349 '1.2.3' 

350 >>> Version("1.2.3+abc").public 

351 '1.2.3' 

352 >>> Version("1.2.3+abc.dev1").public 

353 '1.2.3' 

354 """ 

355 return str(self).split("+", 1)[0] 

356 

357 @property 

358 def base_version(self) -> str: 

359 """The "base version" of the version. 

360 

361 >>> Version("1.2.3").base_version 

362 '1.2.3' 

363 >>> Version("1.2.3+abc").base_version 

364 '1.2.3' 

365 >>> Version("1!1.2.3+abc.dev1").base_version 

366 '1!1.2.3' 

367 

368 The "base version" is the public version of the project without any pre or post 

369 release markers. 

370 """ 

371 parts = [] 

372 

373 # Epoch 

374 if self.epoch != 0: 

375 parts.append(f"{self.epoch}!") 

376 

377 # Release segment 

378 parts.append(".".join(str(x) for x in self.release)) 

379 

380 return "".join(parts) 

381 

382 @property 

383 def is_prerelease(self) -> bool: 

384 """Whether this version is a pre-release. 

385 

386 >>> Version("1.2.3").is_prerelease 

387 False 

388 >>> Version("1.2.3a1").is_prerelease 

389 True 

390 >>> Version("1.2.3b1").is_prerelease 

391 True 

392 >>> Version("1.2.3rc1").is_prerelease 

393 True 

394 >>> Version("1.2.3dev1").is_prerelease 

395 True 

396 """ 

397 return self.dev is not None or self.pre is not None 

398 

399 @property 

400 def is_postrelease(self) -> bool: 

401 """Whether this version is a post-release. 

402 

403 >>> Version("1.2.3").is_postrelease 

404 False 

405 >>> Version("1.2.3.post1").is_postrelease 

406 True 

407 """ 

408 return self.post is not None 

409 

410 @property 

411 def is_devrelease(self) -> bool: 

412 """Whether this version is a development release. 

413 

414 >>> Version("1.2.3").is_devrelease 

415 False 

416 >>> Version("1.2.3.dev1").is_devrelease 

417 True 

418 """ 

419 return self.dev is not None 

420 

421 @property 

422 def major(self) -> int: 

423 """The first item of :attr:`release` or ``0`` if unavailable. 

424 

425 >>> Version("1.2.3").major 

426 1 

427 """ 

428 return self.release[0] if len(self.release) >= 1 else 0 

429 

430 @property 

431 def minor(self) -> int: 

432 """The second item of :attr:`release` or ``0`` if unavailable. 

433 

434 >>> Version("1.2.3").minor 

435 2 

436 >>> Version("1").minor 

437 0 

438 """ 

439 return self.release[1] if len(self.release) >= 2 else 0 

440 

441 @property 

442 def micro(self) -> int: 

443 """The third item of :attr:`release` or ``0`` if unavailable. 

444 

445 >>> Version("1.2.3").micro 

446 3 

447 >>> Version("1").micro 

448 0 

449 """ 

450 return self.release[2] if len(self.release) >= 3 else 0 

451 

452 

453def _parse_letter_version( 

454 letter: str, number: Union[str, bytes, SupportsInt] 

455) -> Optional[Tuple[str, int]]: 

456 

457 if letter: 

458 # We consider there to be an implicit 0 in a pre-release if there is 

459 # not a numeral associated with it. 

460 if number is None: 

461 number = 0 

462 

463 # We normalize any letters to their lower case form 

464 letter = letter.lower() 

465 

466 # We consider some words to be alternate spellings of other words and 

467 # in those cases we want to normalize the spellings to our preferred 

468 # spelling. 

469 if letter == "alpha": 

470 letter = "a" 

471 elif letter == "beta": 

472 letter = "b" 

473 elif letter in ["c", "pre", "preview"]: 

474 letter = "rc" 

475 elif letter in ["rev", "r"]: 

476 letter = "post" 

477 

478 return letter, int(number) 

479 if not letter and number: 

480 # We assume if we are given a number, but we are not given a letter 

481 # then this is using the implicit post release syntax (e.g. 1.0-1) 

482 letter = "post" 

483 

484 return letter, int(number) 

485 

486 return None 

487 

488 

489_local_version_separators = re.compile(r"[\._-]") 

490 

491 

492def _parse_local_version(local: str) -> Optional[LocalType]: 

493 """ 

494 Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). 

495 """ 

496 if local is not None: 

497 return tuple( 

498 part.lower() if not part.isdigit() else int(part) 

499 for part in _local_version_separators.split(local) 

500 ) 

501 return None 

502 

503 

504def _cmpkey( 

505 epoch: int, 

506 release: Tuple[int, ...], 

507 pre: Optional[Tuple[str, int]], 

508 post: Optional[Tuple[str, int]], 

509 dev: Optional[Tuple[str, int]], 

510 local: Optional[Tuple[SubLocalType]], 

511) -> CmpKey: 

512 

513 # When we compare a release version, we want to compare it with all of the 

514 # trailing zeros removed. So we'll use a reverse the list, drop all the now 

515 # leading zeros until we come to something non zero, then take the rest 

516 # re-reverse it back into the correct order and make it a tuple and use 

517 # that for our sorting key. 

518 _release = tuple( 

519 reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) 

520 ) 

521 

522 # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. 

523 # We'll do this by abusing the pre segment, but we _only_ want to do this 

524 # if there is not a pre or a post segment. If we have one of those then 

525 # the normal sorting rules will handle this case correctly. 

526 if pre is None and post is None and dev is not None: 

527 _pre: PrePostDevType = NegativeInfinity 

528 # Versions without a pre-release (except as noted above) should sort after 

529 # those with one. 

530 elif pre is None: 

531 _pre = Infinity 

532 else: 

533 _pre = pre 

534 

535 # Versions without a post segment should sort before those with one. 

536 if post is None: 

537 _post: PrePostDevType = NegativeInfinity 

538 

539 else: 

540 _post = post 

541 

542 # Versions without a development segment should sort after those with one. 

543 if dev is None: 

544 _dev: PrePostDevType = Infinity 

545 

546 else: 

547 _dev = dev 

548 

549 if local is None: 

550 # Versions without a local segment should sort before those with one. 

551 _local: LocalType = NegativeInfinity 

552 else: 

553 # Versions with a local segment need that segment parsed to implement 

554 # the sorting rules in PEP440. 

555 # - Alpha numeric segments sort before numeric segments 

556 # - Alpha numeric segments sort lexicographically 

557 # - Numeric segments sort numerically 

558 # - Shorter versions sort before longer versions when the prefixes 

559 # match exactly 

560 _local = tuple( 

561 (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local 

562 ) 

563 

564 return epoch, _release, _pre, _post, _dev, _local