Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pandas/util/version/__init__.py: 49%

254 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1# Vendored from https://github.com/pypa/packaging/blob/main/packaging/_structures.py 

2# and https://github.com/pypa/packaging/blob/main/packaging/_structures.py 

3# changeset ae891fd74d6dd4c6063bb04f2faeadaac6fc6313 

4# 04/30/2021 

5 

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

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

8# for complete details. 

9from __future__ import annotations 

10 

11import collections 

12import itertools 

13import re 

14from typing import ( 

15 Callable, 

16 Iterator, 

17 SupportsInt, 

18 Tuple, 

19 Union, 

20) 

21import warnings 

22 

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

24 

25 

26class InfinityType: 

27 def __repr__(self) -> str: 

28 return "Infinity" 

29 

30 def __hash__(self) -> int: 

31 return hash(repr(self)) 

32 

33 def __lt__(self, other: object) -> bool: 

34 return False 

35 

36 def __le__(self, other: object) -> bool: 

37 return False 

38 

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

40 return isinstance(other, type(self)) 

41 

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

43 return not isinstance(other, type(self)) 

44 

45 def __gt__(self, other: object) -> bool: 

46 return True 

47 

48 def __ge__(self, other: object) -> bool: 

49 return True 

50 

51 def __neg__(self: object) -> NegativeInfinityType: 

52 return NegativeInfinity 

53 

54 

55Infinity = InfinityType() 

56 

57 

58class NegativeInfinityType: 

59 def __repr__(self) -> str: 

60 return "-Infinity" 

61 

62 def __hash__(self) -> int: 

63 return hash(repr(self)) 

64 

65 def __lt__(self, other: object) -> bool: 

66 return True 

67 

68 def __le__(self, other: object) -> bool: 

69 return True 

70 

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

72 return isinstance(other, type(self)) 

73 

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

75 return not isinstance(other, type(self)) 

76 

77 def __gt__(self, other: object) -> bool: 

78 return False 

79 

80 def __ge__(self, other: object) -> bool: 

81 return False 

82 

83 def __neg__(self: object) -> InfinityType: 

84 return Infinity 

85 

86 

87NegativeInfinity = NegativeInfinityType() 

88 

89 

90InfiniteTypes = Union[InfinityType, NegativeInfinityType] 

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

92SubLocalType = Union[InfiniteTypes, int, str] 

93LocalType = Union[ 

94 NegativeInfinityType, 

95 Tuple[ 

96 Union[ 

97 SubLocalType, 

98 Tuple[SubLocalType, str], 

99 Tuple[NegativeInfinityType, SubLocalType], 

100 ], 

101 ..., 

102 ], 

103] 

104CmpKey = Tuple[ 

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

106] 

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

108VersionComparisonMethod = Callable[ 

109 [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool 

110] 

111 

112_Version = collections.namedtuple( 

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

114) 

115 

116 

117def parse(version: str) -> LegacyVersion | Version: 

118 """ 

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

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

121 a valid PEP 440 version or a legacy version. 

122 """ 

123 try: 

124 return Version(version) 

125 except InvalidVersion: 

126 return LegacyVersion(version) 

127 

128 

129class InvalidVersion(ValueError): 

130 """ 

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

132 """ 

133 

134 

135class _BaseVersion: 

136 _key: CmpKey | LegacyCmpKey 

137 

138 def __hash__(self) -> int: 

139 return hash(self._key) 

140 

141 # Please keep the duplicated `isinstance` check 

142 # in the six comparisons hereunder 

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

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

145 if not isinstance(other, _BaseVersion): 145 ↛ 146line 145 didn't jump to line 146, because the condition on line 145 was never true

146 return NotImplemented 

147 

148 return self._key < other._key 

149 

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

151 if not isinstance(other, _BaseVersion): 

152 return NotImplemented 

153 

154 return self._key <= other._key 

155 

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

157 if not isinstance(other, _BaseVersion): 

158 return NotImplemented 

159 

160 return self._key == other._key 

161 

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

163 if not isinstance(other, _BaseVersion): 163 ↛ 164line 163 didn't jump to line 164, because the condition on line 163 was never true

164 return NotImplemented 

165 

166 return self._key >= other._key 

167 

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

169 if not isinstance(other, _BaseVersion): 

170 return NotImplemented 

171 

172 return self._key > other._key 

173 

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

175 if not isinstance(other, _BaseVersion): 

176 return NotImplemented 

177 

178 return self._key != other._key 

179 

180 

181class LegacyVersion(_BaseVersion): 

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

183 self._version = str(version) 

184 self._key = _legacy_cmpkey(self._version) 

185 

186 warnings.warn( 

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

188 "removed in the next major release.", 

189 DeprecationWarning, 

190 ) 

191 

192 def __str__(self) -> str: 

193 return self._version 

194 

195 def __repr__(self) -> str: 

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

197 

198 @property 

199 def public(self) -> str: 

200 return self._version 

201 

202 @property 

203 def base_version(self) -> str: 

204 return self._version 

205 

206 @property 

207 def epoch(self) -> int: 

208 return -1 

209 

210 @property 

211 def release(self) -> None: 

212 return None 

213 

214 @property 

215 def pre(self) -> None: 

216 return None 

217 

218 @property 

219 def post(self) -> None: 

220 return None 

221 

222 @property 

223 def dev(self) -> None: 

224 return None 

225 

226 @property 

227 def local(self) -> None: 

228 return None 

229 

230 @property 

231 def is_prerelease(self) -> bool: 

232 return False 

233 

234 @property 

235 def is_postrelease(self) -> bool: 

236 return False 

237 

238 @property 

239 def is_devrelease(self) -> bool: 

240 return False 

241 

242 

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

244 

245_legacy_version_replacement_map = { 

246 "pre": "c", 

247 "preview": "c", 

248 "-": "final-", 

249 "rc": "c", 

250 "dev": "@", 

251} 

252 

253 

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

255 for part in _legacy_version_component_re.split(s): 

256 part = _legacy_version_replacement_map.get(part, part) 

257 

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

259 continue 

260 

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

262 # pad for numeric comparison 

263 yield part.zfill(8) 

264 else: 

265 yield "*" + part 

266 

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

268 yield "*final" 

269 

270 

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

272 

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

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

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

276 # as before all PEP 440 versions. 

277 epoch = -1 

278 

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

280 # it's adoption of the packaging library. 

281 parts: list[str] = [] 

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

283 if part.startswith("*"): 

284 # remove "-" before a prerelease tag 

285 if part < "*final": 

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

287 parts.pop() 

288 

289 # remove trailing zeros from each series of numeric parts 

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

291 parts.pop() 

292 

293 parts.append(part) 

294 

295 return epoch, tuple(parts) 

296 

297 

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

299# easier for 3rd party code to reuse 

300VERSION_PATTERN = r""" 

301 v? 

302 (?: 

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

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

305 (?P<pre> # pre-release 

306 [-_\.]? 

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

308 [-_\.]? 

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

310 )? 

311 (?P<post> # post release 

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

313 | 

314 (?: 

315 [-_\.]? 

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

317 [-_\.]? 

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

319 ) 

320 )? 

321 (?P<dev> # dev release 

322 [-_\.]? 

323 (?P<dev_l>dev) 

324 [-_\.]? 

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

326 )? 

327 ) 

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

329""" 

330 

331 

332class Version(_BaseVersion): 

333 

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

335 

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

337 

338 # Validate the version and parse it into pieces 

339 match = self._regex.search(version) 

340 if not match: 340 ↛ 341line 340 didn't jump to line 341, because the condition on line 340 was never true

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

342 

343 # Store the parsed out pieces of the version 

344 self._version = _Version( 

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

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

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

348 post=_parse_letter_version( 

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

350 ), 

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

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

353 ) 

354 

355 # Generate a key which will be used for sorting 

356 self._key = _cmpkey( 

357 self._version.epoch, 

358 self._version.release, 

359 self._version.pre, 

360 self._version.post, 

361 self._version.dev, 

362 self._version.local, 

363 ) 

364 

365 def __repr__(self) -> str: 

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

367 

368 def __str__(self) -> str: 

369 parts = [] 

370 

371 # Epoch 

372 if self.epoch != 0: 

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

374 

375 # Release segment 

376 parts.append(".".join([str(x) for x in self.release])) 

377 

378 # Pre-release 

379 if self.pre is not None: 

380 parts.append("".join([str(x) for x in self.pre])) 

381 

382 # Post-release 

383 if self.post is not None: 

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

385 

386 # Development release 

387 if self.dev is not None: 

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

389 

390 # Local version segment 

391 if self.local is not None: 

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

393 

394 return "".join(parts) 

395 

396 @property 

397 def epoch(self) -> int: 

398 _epoch: int = self._version.epoch 

399 return _epoch 

400 

401 @property 

402 def release(self) -> tuple[int, ...]: 

403 _release: tuple[int, ...] = self._version.release 

404 return _release 

405 

406 @property 

407 def pre(self) -> tuple[str, int] | None: 

408 _pre: tuple[str, int] | None = self._version.pre 

409 return _pre 

410 

411 @property 

412 def post(self) -> int | None: 

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

414 

415 @property 

416 def dev(self) -> int | None: 

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

418 

419 @property 

420 def local(self) -> str | None: 

421 if self._version.local: 

422 return ".".join([str(x) for x in self._version.local]) 

423 else: 

424 return None 

425 

426 @property 

427 def public(self) -> str: 

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

429 

430 @property 

431 def base_version(self) -> str: 

432 parts = [] 

433 

434 # Epoch 

435 if self.epoch != 0: 

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

437 

438 # Release segment 

439 parts.append(".".join([str(x) for x in self.release])) 

440 

441 return "".join(parts) 

442 

443 @property 

444 def is_prerelease(self) -> bool: 

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

446 

447 @property 

448 def is_postrelease(self) -> bool: 

449 return self.post is not None 

450 

451 @property 

452 def is_devrelease(self) -> bool: 

453 return self.dev is not None 

454 

455 @property 

456 def major(self) -> int: 

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

458 

459 @property 

460 def minor(self) -> int: 

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

462 

463 @property 

464 def micro(self) -> int: 

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

466 

467 

468def _parse_letter_version( 

469 letter: str, number: str | bytes | SupportsInt 

470) -> tuple[str, int] | None: 

471 

472 if letter: 472 ↛ 475line 472 didn't jump to line 475, because the condition on line 472 was never true

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

474 # not a numeral associated with it. 

475 if number is None: 

476 number = 0 

477 

478 # We normalize any letters to their lower case form 

479 letter = letter.lower() 

480 

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

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

483 # spelling. 

484 if letter == "alpha": 

485 letter = "a" 

486 elif letter == "beta": 

487 letter = "b" 

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

489 letter = "rc" 

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

491 letter = "post" 

492 

493 return letter, int(number) 

494 if not letter and number: 494 ↛ 497line 494 didn't jump to line 497, because the condition on line 494 was never true

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

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

497 letter = "post" 

498 

499 return letter, int(number) 

500 

501 return None 

502 

503 

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

505 

506 

507def _parse_local_version(local: str) -> LocalType | None: 

508 """ 

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

510 """ 

511 if local is not None: 511 ↛ 512line 511 didn't jump to line 512, because the condition on line 511 was never true

512 return tuple( 

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

514 for part in _local_version_separators.split(local) 

515 ) 

516 return None 

517 

518 

519def _cmpkey( 

520 epoch: int, 

521 release: tuple[int, ...], 

522 pre: tuple[str, int] | None, 

523 post: tuple[str, int] | None, 

524 dev: tuple[str, int] | None, 

525 local: tuple[SubLocalType] | None, 

526) -> CmpKey: 

527 

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

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

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

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

532 # that for our sorting key. 

533 _release = tuple( 

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

535 ) 

536 

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

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

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

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

541 if pre is None and post is None and dev is not None: 541 ↛ 542line 541 didn't jump to line 542, because the condition on line 541 was never true

542 _pre: PrePostDevType = NegativeInfinity 

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

544 # those with one. 

545 elif pre is None: 545 ↛ 548line 545 didn't jump to line 548, because the condition on line 545 was never false

546 _pre = Infinity 

547 else: 

548 _pre = pre 

549 

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

551 if post is None: 551 ↛ 555line 551 didn't jump to line 555, because the condition on line 551 was never false

552 _post: PrePostDevType = NegativeInfinity 

553 

554 else: 

555 _post = post 

556 

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

558 if dev is None: 558 ↛ 562line 558 didn't jump to line 562, because the condition on line 558 was never false

559 _dev: PrePostDevType = Infinity 

560 

561 else: 

562 _dev = dev 

563 

564 if local is None: 564 ↛ 575line 564 didn't jump to line 575, because the condition on line 564 was never false

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

566 _local: LocalType = NegativeInfinity 

567 else: 

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

569 # the sorting rules in PEP440. 

570 # - Alpha numeric segments sort before numeric segments 

571 # - Alpha numeric segments sort lexicographically 

572 # - Numeric segments sort numerically 

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

574 # match exactly 

575 _local = tuple( 

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

577 ) 

578 

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