__init__.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  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. # This file is dual licensed under the terms of the Apache License, Version
  6. # 2.0, and the BSD License. See the LICENSE file in the root of this repository
  7. # for complete details.
  8. from __future__ import annotations
  9. import collections
  10. import itertools
  11. import re
  12. from typing import (
  13. Callable,
  14. Iterator,
  15. SupportsInt,
  16. Tuple,
  17. Union,
  18. )
  19. import warnings
  20. __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
  21. class InfinityType:
  22. def __repr__(self) -> str:
  23. return "Infinity"
  24. def __hash__(self) -> int:
  25. return hash(repr(self))
  26. def __lt__(self, other: object) -> bool:
  27. return False
  28. def __le__(self, other: object) -> bool:
  29. return False
  30. def __eq__(self, other: object) -> bool:
  31. return isinstance(other, type(self))
  32. def __ne__(self, other: object) -> bool:
  33. return not isinstance(other, type(self))
  34. def __gt__(self, other: object) -> bool:
  35. return True
  36. def __ge__(self, other: object) -> bool:
  37. return True
  38. def __neg__(self: object) -> NegativeInfinityType:
  39. return NegativeInfinity
  40. Infinity = InfinityType()
  41. class NegativeInfinityType:
  42. def __repr__(self) -> str:
  43. return "-Infinity"
  44. def __hash__(self) -> int:
  45. return hash(repr(self))
  46. def __lt__(self, other: object) -> bool:
  47. return True
  48. def __le__(self, other: object) -> bool:
  49. return True
  50. def __eq__(self, other: object) -> bool:
  51. return isinstance(other, type(self))
  52. def __ne__(self, other: object) -> bool:
  53. return not isinstance(other, type(self))
  54. def __gt__(self, other: object) -> bool:
  55. return False
  56. def __ge__(self, other: object) -> bool:
  57. return False
  58. def __neg__(self: object) -> InfinityType:
  59. return Infinity
  60. NegativeInfinity = NegativeInfinityType()
  61. InfiniteTypes = Union[InfinityType, NegativeInfinityType]
  62. PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
  63. SubLocalType = Union[InfiniteTypes, int, str]
  64. LocalType = Union[
  65. NegativeInfinityType,
  66. Tuple[
  67. Union[
  68. SubLocalType,
  69. Tuple[SubLocalType, str],
  70. Tuple[NegativeInfinityType, SubLocalType],
  71. ],
  72. ...,
  73. ],
  74. ]
  75. CmpKey = Tuple[
  76. int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
  77. ]
  78. LegacyCmpKey = Tuple[int, Tuple[str, ...]]
  79. VersionComparisonMethod = Callable[
  80. [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
  81. ]
  82. _Version = collections.namedtuple(
  83. "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
  84. )
  85. def parse(version: str) -> LegacyVersion | Version:
  86. """
  87. Parse the given version string and return either a :class:`Version` object
  88. or a :class:`LegacyVersion` object depending on if the given version is
  89. a valid PEP 440 version or a legacy version.
  90. """
  91. try:
  92. return Version(version)
  93. except InvalidVersion:
  94. return LegacyVersion(version)
  95. class InvalidVersion(ValueError):
  96. """
  97. An invalid version was found, users should refer to PEP 440.
  98. """
  99. class _BaseVersion:
  100. _key: CmpKey | LegacyCmpKey
  101. def __hash__(self) -> int:
  102. return hash(self._key)
  103. # Please keep the duplicated `isinstance` check
  104. # in the six comparisons hereunder
  105. # unless you find a way to avoid adding overhead function calls.
  106. def __lt__(self, other: _BaseVersion) -> bool:
  107. if not isinstance(other, _BaseVersion):
  108. return NotImplemented
  109. return self._key < other._key
  110. def __le__(self, other: _BaseVersion) -> bool:
  111. if not isinstance(other, _BaseVersion):
  112. return NotImplemented
  113. return self._key <= other._key
  114. def __eq__(self, other: object) -> bool:
  115. if not isinstance(other, _BaseVersion):
  116. return NotImplemented
  117. return self._key == other._key
  118. def __ge__(self, other: _BaseVersion) -> bool:
  119. if not isinstance(other, _BaseVersion):
  120. return NotImplemented
  121. return self._key >= other._key
  122. def __gt__(self, other: _BaseVersion) -> bool:
  123. if not isinstance(other, _BaseVersion):
  124. return NotImplemented
  125. return self._key > other._key
  126. def __ne__(self, other: object) -> bool:
  127. if not isinstance(other, _BaseVersion):
  128. return NotImplemented
  129. return self._key != other._key
  130. class LegacyVersion(_BaseVersion):
  131. def __init__(self, version: str) -> None:
  132. self._version = str(version)
  133. self._key = _legacy_cmpkey(self._version)
  134. warnings.warn(
  135. "Creating a LegacyVersion has been deprecated and will be "
  136. "removed in the next major release.",
  137. DeprecationWarning,
  138. )
  139. def __str__(self) -> str:
  140. return self._version
  141. def __repr__(self) -> str:
  142. return f"<LegacyVersion('{self}')>"
  143. @property
  144. def public(self) -> str:
  145. return self._version
  146. @property
  147. def base_version(self) -> str:
  148. return self._version
  149. @property
  150. def epoch(self) -> int:
  151. return -1
  152. @property
  153. def release(self) -> None:
  154. return None
  155. @property
  156. def pre(self) -> None:
  157. return None
  158. @property
  159. def post(self) -> None:
  160. return None
  161. @property
  162. def dev(self) -> None:
  163. return None
  164. @property
  165. def local(self) -> None:
  166. return None
  167. @property
  168. def is_prerelease(self) -> bool:
  169. return False
  170. @property
  171. def is_postrelease(self) -> bool:
  172. return False
  173. @property
  174. def is_devrelease(self) -> bool:
  175. return False
  176. _legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
  177. _legacy_version_replacement_map = {
  178. "pre": "c",
  179. "preview": "c",
  180. "-": "final-",
  181. "rc": "c",
  182. "dev": "@",
  183. }
  184. def _parse_version_parts(s: str) -> Iterator[str]:
  185. for part in _legacy_version_component_re.split(s):
  186. part = _legacy_version_replacement_map.get(part, part)
  187. if not part or part == ".":
  188. continue
  189. if part[:1] in "0123456789":
  190. # pad for numeric comparison
  191. yield part.zfill(8)
  192. else:
  193. yield "*" + part
  194. # ensure that alpha/beta/candidate are before final
  195. yield "*final"
  196. def _legacy_cmpkey(version: str) -> LegacyCmpKey:
  197. # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
  198. # greater than or equal to 0. This will effectively put the LegacyVersion,
  199. # which uses the defacto standard originally implemented by setuptools,
  200. # as before all PEP 440 versions.
  201. epoch = -1
  202. # This scheme is taken from pkg_resources.parse_version setuptools prior to
  203. # it's adoption of the packaging library.
  204. parts: list[str] = []
  205. for part in _parse_version_parts(version.lower()):
  206. if part.startswith("*"):
  207. # remove "-" before a prerelease tag
  208. if part < "*final":
  209. while parts and parts[-1] == "*final-":
  210. parts.pop()
  211. # remove trailing zeros from each series of numeric parts
  212. while parts and parts[-1] == "00000000":
  213. parts.pop()
  214. parts.append(part)
  215. return epoch, tuple(parts)
  216. # Deliberately not anchored to the start and end of the string, to make it
  217. # easier for 3rd party code to reuse
  218. VERSION_PATTERN = r"""
  219. v?
  220. (?:
  221. (?:(?P<epoch>[0-9]+)!)? # epoch
  222. (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
  223. (?P<pre> # pre-release
  224. [-_\.]?
  225. (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
  226. [-_\.]?
  227. (?P<pre_n>[0-9]+)?
  228. )?
  229. (?P<post> # post release
  230. (?:-(?P<post_n1>[0-9]+))
  231. |
  232. (?:
  233. [-_\.]?
  234. (?P<post_l>post|rev|r)
  235. [-_\.]?
  236. (?P<post_n2>[0-9]+)?
  237. )
  238. )?
  239. (?P<dev> # dev release
  240. [-_\.]?
  241. (?P<dev_l>dev)
  242. [-_\.]?
  243. (?P<dev_n>[0-9]+)?
  244. )?
  245. )
  246. (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
  247. """
  248. class Version(_BaseVersion):
  249. _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
  250. def __init__(self, version: str) -> None:
  251. # Validate the version and parse it into pieces
  252. match = self._regex.search(version)
  253. if not match:
  254. raise InvalidVersion(f"Invalid version: '{version}'")
  255. # Store the parsed out pieces of the version
  256. self._version = _Version(
  257. epoch=int(match.group("epoch")) if match.group("epoch") else 0,
  258. release=tuple(int(i) for i in match.group("release").split(".")),
  259. pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
  260. post=_parse_letter_version(
  261. match.group("post_l"), match.group("post_n1") or match.group("post_n2")
  262. ),
  263. dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
  264. local=_parse_local_version(match.group("local")),
  265. )
  266. # Generate a key which will be used for sorting
  267. self._key = _cmpkey(
  268. self._version.epoch,
  269. self._version.release,
  270. self._version.pre,
  271. self._version.post,
  272. self._version.dev,
  273. self._version.local,
  274. )
  275. def __repr__(self) -> str:
  276. return f"<Version('{self}')>"
  277. def __str__(self) -> str:
  278. parts = []
  279. # Epoch
  280. if self.epoch != 0:
  281. parts.append(f"{self.epoch}!")
  282. # Release segment
  283. parts.append(".".join([str(x) for x in self.release]))
  284. # Pre-release
  285. if self.pre is not None:
  286. parts.append("".join([str(x) for x in self.pre]))
  287. # Post-release
  288. if self.post is not None:
  289. parts.append(f".post{self.post}")
  290. # Development release
  291. if self.dev is not None:
  292. parts.append(f".dev{self.dev}")
  293. # Local version segment
  294. if self.local is not None:
  295. parts.append(f"+{self.local}")
  296. return "".join(parts)
  297. @property
  298. def epoch(self) -> int:
  299. _epoch: int = self._version.epoch
  300. return _epoch
  301. @property
  302. def release(self) -> tuple[int, ...]:
  303. _release: tuple[int, ...] = self._version.release
  304. return _release
  305. @property
  306. def pre(self) -> tuple[str, int] | None:
  307. _pre: tuple[str, int] | None = self._version.pre
  308. return _pre
  309. @property
  310. def post(self) -> int | None:
  311. return self._version.post[1] if self._version.post else None
  312. @property
  313. def dev(self) -> int | None:
  314. return self._version.dev[1] if self._version.dev else None
  315. @property
  316. def local(self) -> str | None:
  317. if self._version.local:
  318. return ".".join([str(x) for x in self._version.local])
  319. else:
  320. return None
  321. @property
  322. def public(self) -> str:
  323. return str(self).split("+", 1)[0]
  324. @property
  325. def base_version(self) -> str:
  326. parts = []
  327. # Epoch
  328. if self.epoch != 0:
  329. parts.append(f"{self.epoch}!")
  330. # Release segment
  331. parts.append(".".join([str(x) for x in self.release]))
  332. return "".join(parts)
  333. @property
  334. def is_prerelease(self) -> bool:
  335. return self.dev is not None or self.pre is not None
  336. @property
  337. def is_postrelease(self) -> bool:
  338. return self.post is not None
  339. @property
  340. def is_devrelease(self) -> bool:
  341. return self.dev is not None
  342. @property
  343. def major(self) -> int:
  344. return self.release[0] if len(self.release) >= 1 else 0
  345. @property
  346. def minor(self) -> int:
  347. return self.release[1] if len(self.release) >= 2 else 0
  348. @property
  349. def micro(self) -> int:
  350. return self.release[2] if len(self.release) >= 3 else 0
  351. def _parse_letter_version(
  352. letter: str, number: str | bytes | SupportsInt
  353. ) -> tuple[str, int] | None:
  354. if letter:
  355. # We consider there to be an implicit 0 in a pre-release if there is
  356. # not a numeral associated with it.
  357. if number is None:
  358. number = 0
  359. # We normalize any letters to their lower case form
  360. letter = letter.lower()
  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. return letter, int(number)
  373. if not letter and number:
  374. # We assume if we are given a number, but we are not given a letter
  375. # then this is using the implicit post release syntax (e.g. 1.0-1)
  376. letter = "post"
  377. return letter, int(number)
  378. return None
  379. _local_version_separators = re.compile(r"[\._-]")
  380. def _parse_local_version(local: str) -> LocalType | None:
  381. """
  382. Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
  383. """
  384. if local is not None:
  385. return tuple(
  386. part.lower() if not part.isdigit() else int(part)
  387. for part in _local_version_separators.split(local)
  388. )
  389. return None
  390. def _cmpkey(
  391. epoch: int,
  392. release: tuple[int, ...],
  393. pre: tuple[str, int] | None,
  394. post: tuple[str, int] | None,
  395. dev: tuple[str, int] | None,
  396. local: tuple[SubLocalType] | None,
  397. ) -> CmpKey:
  398. # When we compare a release version, we want to compare it with all of the
  399. # trailing zeros removed. So we'll use a reverse the list, drop all the now
  400. # leading zeros until we come to something non zero, then take the rest
  401. # re-reverse it back into the correct order and make it a tuple and use
  402. # that for our sorting key.
  403. _release = tuple(
  404. reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
  405. )
  406. # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
  407. # We'll do this by abusing the pre segment, but we _only_ want to do this
  408. # if there is not a pre or a post segment. If we have one of those then
  409. # the normal sorting rules will handle this case correctly.
  410. if pre is None and post is None and dev is not None:
  411. _pre: PrePostDevType = NegativeInfinity
  412. # Versions without a pre-release (except as noted above) should sort after
  413. # those with one.
  414. elif pre is None:
  415. _pre = Infinity
  416. else:
  417. _pre = pre
  418. # Versions without a post segment should sort before those with one.
  419. if post is None:
  420. _post: PrePostDevType = NegativeInfinity
  421. else:
  422. _post = post
  423. # Versions without a development segment should sort after those with one.
  424. if dev is None:
  425. _dev: PrePostDevType = Infinity
  426. else:
  427. _dev = dev
  428. if local is None:
  429. # Versions without a local segment should sort before those with one.
  430. _local: LocalType = NegativeInfinity
  431. else:
  432. # Versions with a local segment need that segment parsed to implement
  433. # the sorting rules in PEP440.
  434. # - Alpha numeric segments sort before numeric segments
  435. # - Alpha numeric segments sort lexicographically
  436. # - Numeric segments sort numerically
  437. # - Shorter versions sort before longer versions when the prefixes
  438. # match exactly
  439. _local = tuple(
  440. (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
  441. )
  442. return epoch, _release, _pre, _post, _dev, _local