version.py 16 KB

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