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.

536 lines
15KB

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