_pep440.py 14 KB

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