Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pandas/util/version/__init__.py: 52%

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

270 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. 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): 

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): 

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 # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch 

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

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

275 # as before all PEP 440 versions. 

276 epoch = -1 

277 

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

279 # it's adoption of the packaging library. 

280 parts: list[str] = [] 

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

282 if part.startswith("*"): 

283 # remove "-" before a prerelease tag 

284 if part < "*final": 

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

286 parts.pop() 

287 

288 # remove trailing zeros from each series of numeric parts 

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

290 parts.pop() 

291 

292 parts.append(part) 

293 

294 return epoch, tuple(parts) 

295 

296 

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

298# easier for 3rd party code to reuse 

299VERSION_PATTERN = r""" 

300 v? 

301 (?: 

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

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

304 (?P<pre> # pre-release 

305 [-_\.]? 

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

307 [-_\.]? 

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

309 )? 

310 (?P<post> # post release 

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

312 | 

313 (?: 

314 [-_\.]? 

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

316 [-_\.]? 

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

318 ) 

319 )? 

320 (?P<dev> # dev release 

321 [-_\.]? 

322 (?P<dev_l>dev) 

323 [-_\.]? 

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

325 )? 

326 ) 

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

328""" 

329 

330 

331class Version(_BaseVersion): 

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

333 

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

335 # Validate the version and parse it into pieces 

336 match = self._regex.search(version) 

337 if not match: 

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

339 

340 # Store the parsed out pieces of the version 

341 self._version = _Version( 

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

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

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

345 post=_parse_letter_version( 

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

347 ), 

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

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

350 ) 

351 

352 # Generate a key which will be used for sorting 

353 self._key = _cmpkey( 

354 self._version.epoch, 

355 self._version.release, 

356 self._version.pre, 

357 self._version.post, 

358 self._version.dev, 

359 self._version.local, 

360 ) 

361 

362 def __repr__(self) -> str: 

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

364 

365 def __str__(self) -> str: 

366 parts = [] 

367 

368 # Epoch 

369 if self.epoch != 0: 

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

371 

372 # Release segment 

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

374 

375 # Pre-release 

376 if self.pre is not None: 

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

378 

379 # Post-release 

380 if self.post is not None: 

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

382 

383 # Development release 

384 if self.dev is not None: 

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

386 

387 # Local version segment 

388 if self.local is not None: 

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

390 

391 return "".join(parts) 

392 

393 @property 

394 def epoch(self) -> int: 

395 _epoch: int = self._version.epoch 

396 return _epoch 

397 

398 @property 

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

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

401 return _release 

402 

403 @property 

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

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

406 return _pre 

407 

408 @property 

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

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

411 

412 @property 

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

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

415 

416 @property 

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

418 if self._version.local: 

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

420 else: 

421 return None 

422 

423 @property 

424 def public(self) -> str: 

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

426 

427 @property 

428 def base_version(self) -> str: 

429 parts = [] 

430 

431 # Epoch 

432 if self.epoch != 0: 

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

434 

435 # Release segment 

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

437 

438 return "".join(parts) 

439 

440 @property 

441 def is_prerelease(self) -> bool: 

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

443 

444 @property 

445 def is_postrelease(self) -> bool: 

446 return self.post is not None 

447 

448 @property 

449 def is_devrelease(self) -> bool: 

450 return self.dev is not None 

451 

452 @property 

453 def major(self) -> int: 

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

455 

456 @property 

457 def minor(self) -> int: 

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

459 

460 @property 

461 def micro(self) -> int: 

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

463 

464 

465def _parse_letter_version( 

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

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

468 if letter: 

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

470 # not a numeral associated with it. 

471 if number is None: 

472 number = 0 

473 

474 # We normalize any letters to their lower case form 

475 letter = letter.lower() 

476 

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

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

479 # spelling. 

480 if letter == "alpha": 

481 letter = "a" 

482 elif letter == "beta": 

483 letter = "b" 

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

485 letter = "rc" 

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

487 letter = "post" 

488 

489 return letter, int(number) 

490 if not letter and number: 

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

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

493 letter = "post" 

494 

495 return letter, int(number) 

496 

497 return None 

498 

499 

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

501 

502 

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

504 """ 

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

506 """ 

507 if local is not None: 

508 return tuple( 

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

510 for part in _local_version_separators.split(local) 

511 ) 

512 return None 

513 

514 

515def _cmpkey( 

516 epoch: int, 

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

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

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

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

521 local: tuple[SubLocalType] | None, 

522) -> CmpKey: 

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

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

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

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

527 # that for our sorting key. 

528 _release = tuple( 

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

530 ) 

531 

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

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

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

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

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

537 _pre: PrePostDevType = NegativeInfinity 

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

539 # those with one. 

540 elif pre is None: 

541 _pre = Infinity 

542 else: 

543 _pre = pre 

544 

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

546 if post is None: 

547 _post: PrePostDevType = NegativeInfinity 

548 

549 else: 

550 _post = post 

551 

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

553 if dev is None: 

554 _dev: PrePostDevType = Infinity 

555 

556 else: 

557 _dev = dev 

558 

559 if local is None: 

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

561 _local: LocalType = NegativeInfinity 

562 else: 

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

564 # the sorting rules in PEP440. 

565 # - Alpha numeric segments sort before numeric segments 

566 # - Alpha numeric segments sort lexicographically 

567 # - Numeric segments sort numerically 

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

569 # match exactly 

570 _local = tuple( 

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

572 ) 

573 

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