You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

505 lines
14KB

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