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

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

202 statements  

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

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

3# changeset 24e5350b2ff3c5c7a36676c2af5f2cb39fd1baf8 

4 

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

6# 2.0, and the BSD License. Licence at LICENSES/PACKAGING_LICENSE 

7from __future__ import annotations 

8 

9from collections.abc import Callable 

10import itertools 

11import re 

12from typing import ( 

13 Any, 

14 NamedTuple, 

15 SupportsInt, 

16 TypeAlias, 

17) 

18 

19__all__ = ["VERSION_PATTERN", "InvalidVersion", "Version", "parse"] 

20 

21 

22class InfinityType: 

23 def __repr__(self) -> str: 

24 return "Infinity" 

25 

26 def __hash__(self) -> int: 

27 return hash(repr(self)) 

28 

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

30 return False 

31 

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

33 return False 

34 

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

36 return isinstance(other, type(self)) 

37 

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

39 return True 

40 

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

42 return True 

43 

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

45 return NegativeInfinity 

46 

47 

48Infinity = InfinityType() 

49 

50 

51class NegativeInfinityType: 

52 def __repr__(self) -> str: 

53 return "-Infinity" 

54 

55 def __hash__(self) -> int: 

56 return hash(repr(self)) 

57 

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

59 return True 

60 

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

62 return True 

63 

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

65 return isinstance(other, type(self)) 

66 

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

68 return False 

69 

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

71 return False 

72 

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

74 return Infinity 

75 

76 

77NegativeInfinity = NegativeInfinityType() 

78 

79 

80LocalType: TypeAlias = tuple[int | str, ...] 

81 

82CmpPrePostDevType: TypeAlias = InfinityType | NegativeInfinityType | tuple[str, int] 

83CmpLocalType: TypeAlias = ( 

84 NegativeInfinityType 

85 | tuple[tuple[int, str] | tuple[NegativeInfinityType, int | str], ...] 

86) 

87CmpKey: TypeAlias = tuple[ 

88 int, 

89 tuple[int, ...], 

90 CmpPrePostDevType, 

91 CmpPrePostDevType, 

92 CmpPrePostDevType, 

93 CmpLocalType, 

94] 

95VersionComparisonMethod: TypeAlias = Callable[[CmpKey, CmpKey], bool] 

96 

97 

98class _Version(NamedTuple): 

99 epoch: int 

100 release: tuple[int, ...] 

101 dev: tuple[str, int] | None 

102 pre: tuple[str, int] | None 

103 post: tuple[str, int] | None 

104 local: LocalType | None 

105 

106 

107def parse(version: str) -> Version: 

108 return Version(version) 

109 

110 

111# The docstring is from an older version of the packaging library to avoid 

112# errors in the docstring validation. 

113class InvalidVersion(ValueError): 

114 """ 

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

116 

117 The ``InvalidVersion`` exception is raised when a version string is 

118 improperly formatted. Pandas uses this exception to ensure that all 

119 version strings are PEP 440 compliant. 

120 

121 See Also 

122 -------- 

123 util.version.Version : Class for handling and parsing version strings. 

124 

125 Examples 

126 -------- 

127 >>> pd.util.version.Version("1.") 

128 Traceback (most recent call last): 

129 InvalidVersion: Invalid version: '1.' 

130 """ 

131 

132 __module__ = "pandas.errors" 

133 

134 

135class _BaseVersion: 

136 _key: tuple[Any, ...] 

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 

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

182# easier for 3rd party code to reuse 

183_VERSION_PATTERN = r""" 

184 v? 

185 (?: 

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

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

188 (?P<pre> # pre-release 

189 [-_\.]? 

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

191 [-_\.]? 

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

193 )? 

194 (?P<post> # post release 

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

196 | 

197 (?: 

198 [-_\.]? 

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

200 [-_\.]? 

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

202 ) 

203 )? 

204 (?P<dev> # dev release 

205 [-_\.]? 

206 (?P<dev_l>dev) 

207 [-_\.]? 

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

209 )? 

210 ) 

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

212""" 

213 

214VERSION_PATTERN = _VERSION_PATTERN 

215 

216 

217class Version(_BaseVersion): 

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

219 _key: CmpKey 

220 

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

222 # Validate the version and parse it into pieces 

223 match = self._regex.search(version) 

224 if not match: 

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

226 

227 # Store the parsed out pieces of the version 

228 self._version = _Version( 

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

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

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

232 post=_parse_letter_version( 

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

234 ), 

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

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

237 ) 

238 

239 # Generate a key which will be used for sorting 

240 self._key = _cmpkey( 

241 self._version.epoch, 

242 self._version.release, 

243 self._version.pre, 

244 self._version.post, 

245 self._version.dev, 

246 self._version.local, 

247 ) 

248 

249 def __repr__(self) -> str: 

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

251 

252 def __str__(self) -> str: 

253 parts = [] 

254 

255 # Epoch 

256 if self.epoch != 0: 

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

258 

259 # Release segment 

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

261 

262 # Pre-release 

263 if self.pre is not None: 

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

265 

266 # Post-release 

267 if self.post is not None: 

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

269 

270 # Development release 

271 if self.dev is not None: 

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

273 

274 # Local version segment 

275 if self.local is not None: 

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

277 

278 return "".join(parts) 

279 

280 @property 

281 def epoch(self) -> int: 

282 return self._version.epoch 

283 

284 @property 

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

286 return self._version.release 

287 

288 @property 

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

290 return self._version.pre 

291 

292 @property 

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

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

295 

296 @property 

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

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

299 

300 @property 

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

302 if self._version.local: 

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

304 else: 

305 return None 

306 

307 @property 

308 def public(self) -> str: 

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

310 

311 @property 

312 def base_version(self) -> str: 

313 parts = [] 

314 

315 # Epoch 

316 if self.epoch != 0: 

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

318 

319 # Release segment 

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

321 

322 return "".join(parts) 

323 

324 @property 

325 def is_prerelease(self) -> bool: 

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

327 

328 @property 

329 def is_postrelease(self) -> bool: 

330 return self.post is not None 

331 

332 @property 

333 def is_devrelease(self) -> bool: 

334 return self.dev is not None 

335 

336 @property 

337 def major(self) -> int: 

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

339 

340 @property 

341 def minor(self) -> int: 

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

343 

344 @property 

345 def micro(self) -> int: 

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

347 

348 

349def _parse_letter_version( 

350 letter: str | None, number: str | bytes | SupportsInt | None 

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

352 if letter: 

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

354 # not a numeral associated with it. 

355 if number is None: 

356 number = 0 

357 

358 # We normalize any letters to their lower case form 

359 letter = letter.lower() 

360 

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

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

363 # spelling. 

364 if letter == "alpha": 

365 letter = "a" 

366 elif letter == "beta": 

367 letter = "b" 

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

369 letter = "rc" 

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

371 letter = "post" 

372 

373 return letter, int(number) 

374 if not letter and number: 

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

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

377 letter = "post" 

378 

379 return letter, int(number) 

380 

381 return None 

382 

383 

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

385 

386 

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

388 if local is not None: 

389 return tuple( 

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

391 for part in _local_version_separators.split(local) 

392 ) 

393 return None 

394 

395 

396def _cmpkey( 

397 epoch: int, 

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

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

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

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

402 local: LocalType | None, 

403) -> CmpKey: 

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

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

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

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

408 # that for our sorting key. 

409 _release = tuple( 

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

411 ) 

412 

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

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

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

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

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

418 _pre: CmpPrePostDevType = NegativeInfinity 

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

420 # those with one. 

421 elif pre is None: 

422 _pre = Infinity 

423 else: 

424 _pre = pre 

425 

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

427 if post is None: 

428 _post: CmpPrePostDevType = NegativeInfinity 

429 

430 else: 

431 _post = post 

432 

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

434 if dev is None: 

435 _dev: CmpPrePostDevType = Infinity 

436 

437 else: 

438 _dev = dev 

439 

440 if local is None: 

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

442 _local: CmpLocalType = NegativeInfinity 

443 else: 

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

445 # the sorting rules in PEP440. 

446 # - Alpha numeric segments sort before numeric segments 

447 # - Alpha numeric segments sort lexicographically 

448 # - Numeric segments sort numerically 

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

450 # match exactly 

451 _local = tuple( 

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

453 ) 

454 

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