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

230 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:48 +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 

5import collections 

6import itertools 

7import re 

8import warnings 

9from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union 

10 

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

12 

13__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] 

14 

15InfiniteTypes = Union[InfinityType, NegativeInfinityType] 

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

17SubLocalType = Union[InfiniteTypes, int, str] 

18LocalType = Union[ 

19 NegativeInfinityType, 

20 Tuple[ 

21 Union[ 

22 SubLocalType, 

23 Tuple[SubLocalType, str], 

24 Tuple[NegativeInfinityType, SubLocalType], 

25 ], 

26 ..., 

27 ], 

28] 

29CmpKey = Tuple[ 

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

31] 

32LegacyCmpKey = Tuple[int, Tuple[str, ...]] 

33VersionComparisonMethod = Callable[ 

34 [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool 

35] 

36 

37_Version = collections.namedtuple( 

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

39) 

40 

41 

42def parse(version: str) -> Union["LegacyVersion", "Version"]: 

43 """ 

44 Parse the given version string and return either a :class:`Version` object 

45 or a :class:`LegacyVersion` object depending on if the given version is 

46 a valid PEP 440 version or a legacy version. 

47 """ 

48 try: 

49 return Version(version) 

50 except InvalidVersion: 

51 return LegacyVersion(version) 

52 

53 

54class InvalidVersion(ValueError): 

55 """ 

56 An invalid version was found, users should refer to PEP 440. 

57 """ 

58 

59 

60class _BaseVersion: 

61 _key: Union[CmpKey, LegacyCmpKey] 

62 

63 def __hash__(self) -> int: 

64 return hash(self._key) 

65 

66 # Please keep the duplicated `isinstance` check 

67 # in the six comparisons hereunder 

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

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

70 if not isinstance(other, _BaseVersion): 

71 return NotImplemented 

72 

73 return self._key < other._key 

74 

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

76 if not isinstance(other, _BaseVersion): 

77 return NotImplemented 

78 

79 return self._key <= other._key 

80 

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

82 if not isinstance(other, _BaseVersion): 

83 return NotImplemented 

84 

85 return self._key == other._key 

86 

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

88 if not isinstance(other, _BaseVersion): 

89 return NotImplemented 

90 

91 return self._key >= other._key 

92 

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

94 if not isinstance(other, _BaseVersion): 

95 return NotImplemented 

96 

97 return self._key > other._key 

98 

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

100 if not isinstance(other, _BaseVersion): 

101 return NotImplemented 

102 

103 return self._key != other._key 

104 

105 

106class LegacyVersion(_BaseVersion): 

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

108 self._version = str(version) 

109 self._key = _legacy_cmpkey(self._version) 

110 

111 warnings.warn( 

112 "Creating a LegacyVersion has been deprecated and will be " 

113 "removed in the next major release", 

114 DeprecationWarning, 

115 ) 

116 

117 def __str__(self) -> str: 

118 return self._version 

119 

120 def __repr__(self) -> str: 

121 return f"<LegacyVersion('{self}')>" 

122 

123 @property 

124 def public(self) -> str: 

125 return self._version 

126 

127 @property 

128 def base_version(self) -> str: 

129 return self._version 

130 

131 @property 

132 def epoch(self) -> int: 

133 return -1 

134 

135 @property 

136 def release(self) -> None: 

137 return None 

138 

139 @property 

140 def pre(self) -> None: 

141 return None 

142 

143 @property 

144 def post(self) -> None: 

145 return None 

146 

147 @property 

148 def dev(self) -> None: 

149 return None 

150 

151 @property 

152 def local(self) -> None: 

153 return None 

154 

155 @property 

156 def is_prerelease(self) -> bool: 

157 return False 

158 

159 @property 

160 def is_postrelease(self) -> bool: 

161 return False 

162 

163 @property 

164 def is_devrelease(self) -> bool: 

165 return False 

166 

167 

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

169 

170_legacy_version_replacement_map = { 

171 "pre": "c", 

172 "preview": "c", 

173 "-": "final-", 

174 "rc": "c", 

175 "dev": "@", 

176} 

177 

178 

179def _parse_version_parts(s: str) -> Iterator[str]: 

180 for part in _legacy_version_component_re.split(s): 

181 part = _legacy_version_replacement_map.get(part, part) 

182 

183 if not part or part == ".": 

184 continue 

185 

186 if part[:1] in "0123456789": 

187 # pad for numeric comparison 

188 yield part.zfill(8) 

189 else: 

190 yield "*" + part 

191 

192 # ensure that alpha/beta/candidate are before final 

193 yield "*final" 

194 

195 

196def _legacy_cmpkey(version: str) -> LegacyCmpKey: 

197 

198 # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch 

199 # greater than or equal to 0. This will effectively put the LegacyVersion, 

200 # which uses the defacto standard originally implemented by setuptools, 

201 # as before all PEP 440 versions. 

202 epoch = -1 

203 

204 # This scheme is taken from pkg_resources.parse_version setuptools prior to 

205 # it's adoption of the packaging library. 

206 parts: List[str] = [] 

207 for part in _parse_version_parts(version.lower()): 

208 if part.startswith("*"): 

209 # remove "-" before a prerelease tag 

210 if part < "*final": 

211 while parts and parts[-1] == "*final-": 

212 parts.pop() 

213 

214 # remove trailing zeros from each series of numeric parts 

215 while parts and parts[-1] == "00000000": 

216 parts.pop() 

217 

218 parts.append(part) 

219 

220 return epoch, tuple(parts) 

221 

222 

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

224# easier for 3rd party code to reuse 

225VERSION_PATTERN = r""" 

226 v? 

227 (?: 

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

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

230 (?P<pre> # pre-release 

231 [-_\.]? 

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

233 [-_\.]? 

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

235 )? 

236 (?P<post> # post release 

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

238 | 

239 (?: 

240 [-_\.]? 

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

242 [-_\.]? 

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

244 ) 

245 )? 

246 (?P<dev> # dev release 

247 [-_\.]? 

248 (?P<dev_l>dev) 

249 [-_\.]? 

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

251 )? 

252 ) 

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

254""" 

255 

256 

257class Version(_BaseVersion): 

258 

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

260 

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

262 

263 # Validate the version and parse it into pieces 

264 match = self._regex.search(version) 

265 if not match: 

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

267 

268 # Store the parsed out pieces of the version 

269 self._version = _Version( 

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

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

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

273 post=_parse_letter_version( 

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

275 ), 

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

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

278 ) 

279 

280 # Generate a key which will be used for sorting 

281 self._key = _cmpkey( 

282 self._version.epoch, 

283 self._version.release, 

284 self._version.pre, 

285 self._version.post, 

286 self._version.dev, 

287 self._version.local, 

288 ) 

289 

290 def __repr__(self) -> str: 

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

292 

293 def __str__(self) -> str: 

294 parts = [] 

295 

296 # Epoch 

297 if self.epoch != 0: 

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

299 

300 # Release segment 

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

302 

303 # Pre-release 

304 if self.pre is not None: 

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

306 

307 # Post-release 

308 if self.post is not None: 

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

310 

311 # Development release 

312 if self.dev is not None: 

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

314 

315 # Local version segment 

316 if self.local is not None: 

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

318 

319 return "".join(parts) 

320 

321 @property 

322 def epoch(self) -> int: 

323 _epoch: int = self._version.epoch 

324 return _epoch 

325 

326 @property 

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

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

329 return _release 

330 

331 @property 

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

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

334 return _pre 

335 

336 @property 

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

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

339 

340 @property 

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

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

343 

344 @property 

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

346 if self._version.local: 

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

348 else: 

349 return None 

350 

351 @property 

352 def public(self) -> str: 

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

354 

355 @property 

356 def base_version(self) -> str: 

357 parts = [] 

358 

359 # Epoch 

360 if self.epoch != 0: 

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

362 

363 # Release segment 

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

365 

366 return "".join(parts) 

367 

368 @property 

369 def is_prerelease(self) -> bool: 

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

371 

372 @property 

373 def is_postrelease(self) -> bool: 

374 return self.post is not None 

375 

376 @property 

377 def is_devrelease(self) -> bool: 

378 return self.dev is not None 

379 

380 @property 

381 def major(self) -> int: 

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

383 

384 @property 

385 def minor(self) -> int: 

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

387 

388 @property 

389 def micro(self) -> int: 

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

391 

392 

393def _parse_letter_version( 

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

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

396 

397 if letter: 

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

399 # not a numeral associated with it. 

400 if number is None: 

401 number = 0 

402 

403 # We normalize any letters to their lower case form 

404 letter = letter.lower() 

405 

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

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

408 # spelling. 

409 if letter == "alpha": 

410 letter = "a" 

411 elif letter == "beta": 

412 letter = "b" 

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

414 letter = "rc" 

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

416 letter = "post" 

417 

418 return letter, int(number) 

419 if not letter and number: 

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

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

422 letter = "post" 

423 

424 return letter, int(number) 

425 

426 return None 

427 

428 

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

430 

431 

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

433 """ 

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

435 """ 

436 if local is not None: 

437 return tuple( 

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

439 for part in _local_version_separators.split(local) 

440 ) 

441 return None 

442 

443 

444def _cmpkey( 

445 epoch: int, 

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

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

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

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

450 local: Optional[Tuple[SubLocalType]], 

451) -> CmpKey: 

452 

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

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

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

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

457 # that for our sorting key. 

458 _release = tuple( 

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

460 ) 

461 

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

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

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

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

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

467 _pre: PrePostDevType = NegativeInfinity 

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

469 # those with one. 

470 elif pre is None: 

471 _pre = Infinity 

472 else: 

473 _pre = pre 

474 

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

476 if post is None: 

477 _post: PrePostDevType = NegativeInfinity 

478 

479 else: 

480 _post = post 

481 

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

483 if dev is None: 

484 _dev: PrePostDevType = Infinity 

485 

486 else: 

487 _dev = dev 

488 

489 if local is None: 

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

491 _local: LocalType = NegativeInfinity 

492 else: 

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

494 # the sorting rules in PEP440. 

495 # - Alpha numeric segments sort before numeric segments 

496 # - Alpha numeric segments sort lexicographically 

497 # - Numeric segments sort numerically 

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

499 # match exactly 

500 _local = tuple( 

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

502 ) 

503 

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