Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/pandas/util/version/__init__.py: 55%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

271 statements  

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. Licence at LICENSES/PACKAGING_LICENSE 

8from __future__ import annotations 

9 

10import collections 

11from collections.abc import Iterator 

12import itertools 

13import re 

14from typing import ( 

15 Callable, 

16 SupportsInt, 

17 Tuple, 

18 Union, 

19) 

20import warnings 

21 

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

23 

24 

25class InfinityType: 

26 def __repr__(self) -> str: 

27 return "Infinity" 

28 

29 def __hash__(self) -> int: 

30 return hash(repr(self)) 

31 

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

33 return False 

34 

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

36 return False 

37 

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

39 return isinstance(other, type(self)) 

40 

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

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

43 

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

45 return True 

46 

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

48 return True 

49 

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

51 return NegativeInfinity 

52 

53 

54Infinity = InfinityType() 

55 

56 

57class NegativeInfinityType: 

58 def __repr__(self) -> str: 

59 return "-Infinity" 

60 

61 def __hash__(self) -> int: 

62 return hash(repr(self)) 

63 

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

65 return True 

66 

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

68 return True 

69 

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

71 return isinstance(other, type(self)) 

72 

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

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

75 

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

77 return False 

78 

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

80 return False 

81 

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

83 return Infinity 

84 

85 

86NegativeInfinity = NegativeInfinityType() 

87 

88 

89InfiniteTypes = Union[InfinityType, NegativeInfinityType] 

90PrePostDevType = Union[InfiniteTypes, tuple[str, int]] 

91SubLocalType = Union[InfiniteTypes, int, str] 

92LocalType = Union[ 

93 NegativeInfinityType, 

94 tuple[ 

95 Union[ 

96 SubLocalType, 

97 tuple[SubLocalType, str], 

98 tuple[NegativeInfinityType, SubLocalType], 

99 ], 

100 ..., 

101 ], 

102] 

103CmpKey = tuple[ 

104 int, tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType 

105] 

106LegacyCmpKey = tuple[int, tuple[str, ...]] 

107VersionComparisonMethod = Callable[ 

108 [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool 

109] 

110 

111_Version = collections.namedtuple( 

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

113) 

114 

115 

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

117 """ 

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

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

120 a valid PEP 440 version or a legacy version. 

121 """ 

122 try: 

123 return Version(version) 

124 except InvalidVersion: 

125 return LegacyVersion(version) 

126 

127 

128class InvalidVersion(ValueError): 

129 """ 

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

131 

132 Examples 

133 -------- 

134 >>> pd.util.version.Version('1.') 

135 Traceback (most recent call last): 

136 InvalidVersion: Invalid version: '1.' 

137 """ 

138 

139 

140class _BaseVersion: 

141 _key: CmpKey | LegacyCmpKey 

142 

143 def __hash__(self) -> int: 

144 return hash(self._key) 

145 

146 # Please keep the duplicated `isinstance` check 

147 # in the six comparisons hereunder 

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

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

150 if not isinstance(other, _BaseVersion): 

151 return NotImplemented 

152 

153 return self._key < other._key 

154 

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

156 if not isinstance(other, _BaseVersion): 

157 return NotImplemented 

158 

159 return self._key <= other._key 

160 

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

162 if not isinstance(other, _BaseVersion): 

163 return NotImplemented 

164 

165 return self._key == other._key 

166 

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

168 if not isinstance(other, _BaseVersion): 

169 return NotImplemented 

170 

171 return self._key >= other._key 

172 

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

174 if not isinstance(other, _BaseVersion): 

175 return NotImplemented 

176 

177 return self._key > other._key 

178 

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

180 if not isinstance(other, _BaseVersion): 

181 return NotImplemented 

182 

183 return self._key != other._key 

184 

185 

186class LegacyVersion(_BaseVersion): 

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

188 self._version = str(version) 

189 self._key = _legacy_cmpkey(self._version) 

190 

191 warnings.warn( 

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

193 "removed in the next major release.", 

194 DeprecationWarning, 

195 ) 

196 

197 def __str__(self) -> str: 

198 return self._version 

199 

200 def __repr__(self) -> str: 

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

202 

203 @property 

204 def public(self) -> str: 

205 return self._version 

206 

207 @property 

208 def base_version(self) -> str: 

209 return self._version 

210 

211 @property 

212 def epoch(self) -> int: 

213 return -1 

214 

215 @property 

216 def release(self) -> None: 

217 return None 

218 

219 @property 

220 def pre(self) -> None: 

221 return None 

222 

223 @property 

224 def post(self) -> None: 

225 return None 

226 

227 @property 

228 def dev(self) -> None: 

229 return None 

230 

231 @property 

232 def local(self) -> None: 

233 return None 

234 

235 @property 

236 def is_prerelease(self) -> bool: 

237 return False 

238 

239 @property 

240 def is_postrelease(self) -> bool: 

241 return False 

242 

243 @property 

244 def is_devrelease(self) -> bool: 

245 return False 

246 

247 

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

249 

250_legacy_version_replacement_map = { 

251 "pre": "c", 

252 "preview": "c", 

253 "-": "final-", 

254 "rc": "c", 

255 "dev": "@", 

256} 

257 

258 

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

260 for part in _legacy_version_component_re.split(s): 

261 mapped_part = _legacy_version_replacement_map.get(part, part) 

262 

263 if not mapped_part or mapped_part == ".": 

264 continue 

265 

266 if mapped_part[:1] in "0123456789": 

267 # pad for numeric comparison 

268 yield mapped_part.zfill(8) 

269 else: 

270 yield "*" + mapped_part 

271 

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

273 yield "*final" 

274 

275 

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

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

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

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

280 # as before all PEP 440 versions. 

281 epoch = -1 

282 

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

284 # it's adoption of the packaging library. 

285 parts: list[str] = [] 

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

287 if part.startswith("*"): 

288 # remove "-" before a prerelease tag 

289 if part < "*final": 

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

291 parts.pop() 

292 

293 # remove trailing zeros from each series of numeric parts 

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

295 parts.pop() 

296 

297 parts.append(part) 

298 

299 return epoch, tuple(parts) 

300 

301 

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

303# easier for 3rd party code to reuse 

304VERSION_PATTERN = r""" 

305 v? 

306 (?: 

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

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

309 (?P<pre> # pre-release 

310 [-_\.]? 

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

312 [-_\.]? 

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

314 )? 

315 (?P<post> # post release 

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

317 | 

318 (?: 

319 [-_\.]? 

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

321 [-_\.]? 

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

323 ) 

324 )? 

325 (?P<dev> # dev release 

326 [-_\.]? 

327 (?P<dev_l>dev) 

328 [-_\.]? 

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

330 )? 

331 ) 

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

333""" 

334 

335 

336class Version(_BaseVersion): 

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

338 

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

340 # Validate the version and parse it into pieces 

341 match = self._regex.search(version) 

342 if not match: 

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

344 

345 # Store the parsed out pieces of the version 

346 self._version = _Version( 

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

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

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

350 post=_parse_letter_version( 

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

352 ), 

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

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

355 ) 

356 

357 # Generate a key which will be used for sorting 

358 self._key = _cmpkey( 

359 self._version.epoch, 

360 self._version.release, 

361 self._version.pre, 

362 self._version.post, 

363 self._version.dev, 

364 self._version.local, 

365 ) 

366 

367 def __repr__(self) -> str: 

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

369 

370 def __str__(self) -> str: 

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 # Pre-release 

381 if self.pre is not None: 

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

383 

384 # Post-release 

385 if self.post is not None: 

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

387 

388 # Development release 

389 if self.dev is not None: 

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

391 

392 # Local version segment 

393 if self.local is not None: 

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

395 

396 return "".join(parts) 

397 

398 @property 

399 def epoch(self) -> int: 

400 _epoch: int = self._version.epoch 

401 return _epoch 

402 

403 @property 

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

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

406 return _release 

407 

408 @property 

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

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

411 return _pre 

412 

413 @property 

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

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

416 

417 @property 

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

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

420 

421 @property 

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

423 if self._version.local: 

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

425 else: 

426 return None 

427 

428 @property 

429 def public(self) -> str: 

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

431 

432 @property 

433 def base_version(self) -> str: 

434 parts = [] 

435 

436 # Epoch 

437 if self.epoch != 0: 

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

439 

440 # Release segment 

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

442 

443 return "".join(parts) 

444 

445 @property 

446 def is_prerelease(self) -> bool: 

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

448 

449 @property 

450 def is_postrelease(self) -> bool: 

451 return self.post is not None 

452 

453 @property 

454 def is_devrelease(self) -> bool: 

455 return self.dev is not None 

456 

457 @property 

458 def major(self) -> int: 

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

460 

461 @property 

462 def minor(self) -> int: 

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

464 

465 @property 

466 def micro(self) -> int: 

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

468 

469 

470def _parse_letter_version( 

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

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

473 if letter: 

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

475 # not a numeral associated with it. 

476 if number is None: 

477 number = 0 

478 

479 # We normalize any letters to their lower case form 

480 letter = letter.lower() 

481 

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

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

484 # spelling. 

485 if letter == "alpha": 

486 letter = "a" 

487 elif letter == "beta": 

488 letter = "b" 

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

490 letter = "rc" 

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

492 letter = "post" 

493 

494 return letter, int(number) 

495 if not letter and number: 

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

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

498 letter = "post" 

499 

500 return letter, int(number) 

501 

502 return None 

503 

504 

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

506 

507 

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

509 """ 

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

511 """ 

512 if local is not None: 

513 return tuple( 

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

515 for part in _local_version_separators.split(local) 

516 ) 

517 return None 

518 

519 

520def _cmpkey( 

521 epoch: int, 

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

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

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

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

526 local: tuple[SubLocalType] | None, 

527) -> CmpKey: 

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: 

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: 

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: 

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: 

559 _dev: PrePostDevType = Infinity 

560 

561 else: 

562 _dev = dev 

563 

564 if local is None: 

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