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

162 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +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 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: CmpKey 

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 

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

184 """Initialize a Version object. 

185 

186 :param version: 

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

188 before use. 

189 :raises InvalidVersion: 

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

191 exception will be raised. 

192 """ 

193 

194 # Validate the version and parse it into pieces 

195 match = self._regex.search(version) 

196 if not match: 

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

198 

199 # Store the parsed out pieces of the version 

200 self._version = _Version( 

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

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

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

204 post=_parse_letter_version( 

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

206 ), 

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

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

209 ) 

210 

211 # Generate a key which will be used for sorting 

212 self._key = _cmpkey( 

213 self._version.epoch, 

214 self._version.release, 

215 self._version.pre, 

216 self._version.post, 

217 self._version.dev, 

218 self._version.local, 

219 ) 

220 

221 def __repr__(self) -> str: 

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

223 

224 >>> Version('1.0.0') 

225 <Version('1.0.0')> 

226 """ 

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

228 

229 def __str__(self) -> str: 

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

231 

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

233 '1.0a5' 

234 """ 

235 parts = [] 

236 

237 # Epoch 

238 if self.epoch != 0: 

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

240 

241 # Release segment 

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

243 

244 # Pre-release 

245 if self.pre is not None: 

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

247 

248 # Post-release 

249 if self.post is not None: 

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

251 

252 # Development release 

253 if self.dev is not None: 

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

255 

256 # Local version segment 

257 if self.local is not None: 

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

259 

260 return "".join(parts) 

261 

262 @property 

263 def epoch(self) -> int: 

264 """The epoch of the version. 

265 

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

267 0 

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

269 1 

270 """ 

271 _epoch: int = self._version.epoch 

272 return _epoch 

273 

274 @property 

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

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

277 

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

279 (1, 2, 3) 

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

281 (2, 0, 0) 

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

283 (2, 0, 0) 

284 

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

286 post-release suffixes. 

287 """ 

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

289 return _release 

290 

291 @property 

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

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

294 

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

296 None 

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

298 ('a', 1) 

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

300 ('b', 1) 

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

302 ('rc', 1) 

303 """ 

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

305 return _pre 

306 

307 @property 

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

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

310 

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

312 None 

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

314 1 

315 """ 

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

317 

318 @property 

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

320 """The development number of the version. 

321 

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

323 None 

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

325 1 

326 """ 

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

328 

329 @property 

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

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

332 

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

334 None 

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

336 'abc' 

337 """ 

338 if self._version.local: 

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

340 else: 

341 return None 

342 

343 @property 

344 def public(self) -> str: 

345 """The public portion of the version. 

346 

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

348 '1.2.3' 

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

350 '1.2.3' 

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

352 '1.2.3' 

353 """ 

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

355 

356 @property 

357 def base_version(self) -> str: 

358 """The "base version" of the version. 

359 

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

361 '1.2.3' 

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

363 '1.2.3' 

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

365 '1!1.2.3' 

366 

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

368 release markers. 

369 """ 

370 parts = [] 

371 

372 # Epoch 

373 if self.epoch != 0: 

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

375 

376 # Release segment 

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

378 

379 return "".join(parts) 

380 

381 @property 

382 def is_prerelease(self) -> bool: 

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

384 

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

386 False 

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

388 True 

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

390 True 

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

392 True 

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

394 True 

395 """ 

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

397 

398 @property 

399 def is_postrelease(self) -> bool: 

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

401 

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

403 False 

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

405 True 

406 """ 

407 return self.post is not None 

408 

409 @property 

410 def is_devrelease(self) -> bool: 

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

412 

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

414 False 

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

416 True 

417 """ 

418 return self.dev is not None 

419 

420 @property 

421 def major(self) -> int: 

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

423 

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

425 1 

426 """ 

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

428 

429 @property 

430 def minor(self) -> int: 

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

432 

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

434 2 

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

436 0 

437 """ 

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

439 

440 @property 

441 def micro(self) -> int: 

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

443 

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

445 3 

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

447 0 

448 """ 

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

450 

451 

452def _parse_letter_version( 

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

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

455 

456 if letter: 

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

458 # not a numeral associated with it. 

459 if number is None: 

460 number = 0 

461 

462 # We normalize any letters to their lower case form 

463 letter = letter.lower() 

464 

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

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

467 # spelling. 

468 if letter == "alpha": 

469 letter = "a" 

470 elif letter == "beta": 

471 letter = "b" 

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

473 letter = "rc" 

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

475 letter = "post" 

476 

477 return letter, int(number) 

478 if not letter and number: 

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

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

481 letter = "post" 

482 

483 return letter, int(number) 

484 

485 return None 

486 

487 

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

489 

490 

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

492 """ 

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

494 """ 

495 if local is not None: 

496 return tuple( 

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

498 for part in _local_version_separators.split(local) 

499 ) 

500 return None 

501 

502 

503def _cmpkey( 

504 epoch: int, 

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

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

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

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

509 local: Optional[Tuple[SubLocalType]], 

510) -> CmpKey: 

511 

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

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

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

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

516 # that for our sorting key. 

517 _release = tuple( 

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

519 ) 

520 

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

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

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

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

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

526 _pre: PrePostDevType = NegativeInfinity 

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

528 # those with one. 

529 elif pre is None: 

530 _pre = Infinity 

531 else: 

532 _pre = pre 

533 

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

535 if post is None: 

536 _post: PrePostDevType = NegativeInfinity 

537 

538 else: 

539 _post = post 

540 

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

542 if dev is None: 

543 _dev: PrePostDevType = Infinity 

544 

545 else: 

546 _dev = dev 

547 

548 if local is None: 

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

550 _local: LocalType = NegativeInfinity 

551 else: 

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

553 # the sorting rules in PEP440. 

554 # - Alpha numeric segments sort before numeric segments 

555 # - Alpha numeric segments sort lexicographically 

556 # - Numeric segments sort numerically 

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

558 # match exactly 

559 _local = tuple( 

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

561 ) 

562 

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