version.py 16 KB

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