version.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2012-2016 The Python Software Foundation.
  4. # See LICENSE.txt and CONTRIBUTORS.txt.
  5. #
  6. """
  7. Implementation of a flexible versioning scheme providing support for PEP-440,
  8. setuptools-compatible and semantic versioning.
  9. """
  10. import logging
  11. import re
  12. from .compat import string_types
  13. __all__ = ['NormalizedVersion', 'NormalizedMatcher',
  14. 'LegacyVersion', 'LegacyMatcher',
  15. 'SemanticVersion', 'SemanticMatcher',
  16. 'UnsupportedVersionError', 'get_scheme']
  17. logger = logging.getLogger(__name__)
  18. class UnsupportedVersionError(ValueError):
  19. """This is an unsupported version."""
  20. pass
  21. class Version(object):
  22. def __init__(self, s):
  23. self._string = s = s.strip()
  24. self._parts = parts = self.parse(s)
  25. assert isinstance(parts, tuple)
  26. assert len(parts) > 0
  27. def parse(self, s):
  28. raise NotImplementedError('please implement in a subclass')
  29. def _check_compatible(self, other):
  30. if type(self) != type(other):
  31. raise TypeError('cannot compare %r and %r' % (self, other))
  32. def __eq__(self, other):
  33. self._check_compatible(other)
  34. return self._parts == other._parts
  35. def __ne__(self, other):
  36. return not self.__eq__(other)
  37. def __lt__(self, other):
  38. self._check_compatible(other)
  39. return self._parts < other._parts
  40. def __gt__(self, other):
  41. return not (self.__lt__(other) or self.__eq__(other))
  42. def __le__(self, other):
  43. return self.__lt__(other) or self.__eq__(other)
  44. def __ge__(self, other):
  45. return self.__gt__(other) or self.__eq__(other)
  46. # See http://docs.python.org/reference/datamodel#object.__hash__
  47. def __hash__(self):
  48. return hash(self._parts)
  49. def __repr__(self):
  50. return "%s('%s')" % (self.__class__.__name__, self._string)
  51. def __str__(self):
  52. return self._string
  53. @property
  54. def is_prerelease(self):
  55. raise NotImplementedError('Please implement in subclasses.')
  56. class Matcher(object):
  57. version_class = None
  58. dist_re = re.compile(r"^(\w[\s\w'.-]*)(\((.*)\))?")
  59. comp_re = re.compile(r'^(<=|>=|<|>|!=|={2,3}|~=)?\s*([^\s,]+)$')
  60. num_re = re.compile(r'^\d+(\.\d+)*$')
  61. # value is either a callable or the name of a method
  62. _operators = {
  63. '<': lambda v, c, p: v < c,
  64. '>': lambda v, c, p: v > c,
  65. '<=': lambda v, c, p: v == c or v < c,
  66. '>=': lambda v, c, p: v == c or v > c,
  67. '==': lambda v, c, p: v == c,
  68. '===': lambda v, c, p: v == c,
  69. # by default, compatible => >=.
  70. '~=': lambda v, c, p: v == c or v > c,
  71. '!=': lambda v, c, p: v != c,
  72. }
  73. def __init__(self, s):
  74. if self.version_class is None:
  75. raise ValueError('Please specify a version class')
  76. self._string = s = s.strip()
  77. m = self.dist_re.match(s)
  78. if not m:
  79. raise ValueError('Not valid: %r' % s)
  80. groups = m.groups('')
  81. self.name = groups[0].strip()
  82. self.key = self.name.lower() # for case-insensitive comparisons
  83. clist = []
  84. if groups[2]:
  85. constraints = [c.strip() for c in groups[2].split(',')]
  86. for c in constraints:
  87. m = self.comp_re.match(c)
  88. if not m:
  89. raise ValueError('Invalid %r in %r' % (c, s))
  90. groups = m.groups()
  91. op = groups[0] or '~='
  92. s = groups[1]
  93. if s.endswith('.*'):
  94. if op not in ('==', '!='):
  95. raise ValueError('\'.*\' not allowed for '
  96. '%r constraints' % op)
  97. # Could be a partial version (e.g. for '2.*') which
  98. # won't parse as a version, so keep it as a string
  99. vn, prefix = s[:-2], True
  100. if not self.num_re.match(vn):
  101. # Just to check that vn is a valid version
  102. self.version_class(vn)
  103. else:
  104. # Should parse as a version, so we can create an
  105. # instance for the comparison
  106. vn, prefix = self.version_class(s), False
  107. clist.append((op, vn, prefix))
  108. self._parts = tuple(clist)
  109. def match(self, version):
  110. """
  111. Check if the provided version matches the constraints.
  112. :param version: The version to match against this instance.
  113. :type version: String or :class:`Version` instance.
  114. """
  115. if isinstance(version, string_types):
  116. version = self.version_class(version)
  117. for operator, constraint, prefix in self._parts:
  118. f = self._operators.get(operator)
  119. if isinstance(f, string_types):
  120. f = getattr(self, f)
  121. if not f:
  122. msg = ('%r not implemented '
  123. 'for %s' % (operator, self.__class__.__name__))
  124. raise NotImplementedError(msg)
  125. if not f(version, constraint, prefix):
  126. return False
  127. return True
  128. @property
  129. def exact_version(self):
  130. result = None
  131. if len(self._parts) == 1 and self._parts[0][0] in ('==', '==='):
  132. result = self._parts[0][1]
  133. return result
  134. def _check_compatible(self, other):
  135. if type(self) != type(other) or self.name != other.name:
  136. raise TypeError('cannot compare %s and %s' % (self, other))
  137. def __eq__(self, other):
  138. self._check_compatible(other)
  139. return self.key == other.key and self._parts == other._parts
  140. def __ne__(self, other):
  141. return not self.__eq__(other)
  142. # See http://docs.python.org/reference/datamodel#object.__hash__
  143. def __hash__(self):
  144. return hash(self.key) + hash(self._parts)
  145. def __repr__(self):
  146. return "%s(%r)" % (self.__class__.__name__, self._string)
  147. def __str__(self):
  148. return self._string
  149. PEP440_VERSION_RE = re.compile(r'^v?(\d+!)?(\d+(\.\d+)*)((a|b|c|rc)(\d+))?'
  150. r'(\.(post)(\d+))?(\.(dev)(\d+))?'
  151. r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$')
  152. def _pep_440_key(s):
  153. s = s.strip()
  154. m = PEP440_VERSION_RE.match(s)
  155. if not m:
  156. raise UnsupportedVersionError('Not a valid version: %s' % s)
  157. groups = m.groups()
  158. nums = tuple(int(v) for v in groups[1].split('.'))
  159. while len(nums) > 1 and nums[-1] == 0:
  160. nums = nums[:-1]
  161. if not groups[0]:
  162. epoch = 0
  163. else:
  164. epoch = int(groups[0])
  165. pre = groups[4:6]
  166. post = groups[7:9]
  167. dev = groups[10:12]
  168. local = groups[13]
  169. if pre == (None, None):
  170. pre = ()
  171. else:
  172. pre = pre[0], int(pre[1])
  173. if post == (None, None):
  174. post = ()
  175. else:
  176. post = post[0], int(post[1])
  177. if dev == (None, None):
  178. dev = ()
  179. else:
  180. dev = dev[0], int(dev[1])
  181. if local is None:
  182. local = ()
  183. else:
  184. parts = []
  185. for part in local.split('.'):
  186. # to ensure that numeric compares as > lexicographic, avoid
  187. # comparing them directly, but encode a tuple which ensures
  188. # correct sorting
  189. if part.isdigit():
  190. part = (1, int(part))
  191. else:
  192. part = (0, part)
  193. parts.append(part)
  194. local = tuple(parts)
  195. if not pre:
  196. # either before pre-release, or final release and after
  197. if not post and dev:
  198. # before pre-release
  199. pre = ('a', -1) # to sort before a0
  200. else:
  201. pre = ('z',) # to sort after all pre-releases
  202. # now look at the state of post and dev.
  203. if not post:
  204. post = ('_',) # sort before 'a'
  205. if not dev:
  206. dev = ('final',)
  207. #print('%s -> %s' % (s, m.groups()))
  208. return epoch, nums, pre, post, dev, local
  209. _normalized_key = _pep_440_key
  210. class NormalizedVersion(Version):
  211. """A rational version.
  212. Good:
  213. 1.2 # equivalent to "1.2.0"
  214. 1.2.0
  215. 1.2a1
  216. 1.2.3a2
  217. 1.2.3b1
  218. 1.2.3c1
  219. 1.2.3.4
  220. TODO: fill this out
  221. Bad:
  222. 1 # minimum two numbers
  223. 1.2a # release level must have a release serial
  224. 1.2.3b
  225. """
  226. def parse(self, s):
  227. result = _normalized_key(s)
  228. # _normalized_key loses trailing zeroes in the release
  229. # clause, since that's needed to ensure that X.Y == X.Y.0 == X.Y.0.0
  230. # However, PEP 440 prefix matching needs it: for example,
  231. # (~= 1.4.5.0) matches differently to (~= 1.4.5.0.0).
  232. m = PEP440_VERSION_RE.match(s) # must succeed
  233. groups = m.groups()
  234. self._release_clause = tuple(int(v) for v in groups[1].split('.'))
  235. return result
  236. PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev'])
  237. @property
  238. def is_prerelease(self):
  239. return any(t[0] in self.PREREL_TAGS for t in self._parts if t)
  240. def _match_prefix(x, y):
  241. x = str(x)
  242. y = str(y)
  243. if x == y:
  244. return True
  245. if not x.startswith(y):
  246. return False
  247. n = len(y)
  248. return x[n] == '.'
  249. class NormalizedMatcher(Matcher):
  250. version_class = NormalizedVersion
  251. # value is either a callable or the name of a method
  252. _operators = {
  253. '~=': '_match_compatible',
  254. '<': '_match_lt',
  255. '>': '_match_gt',
  256. '<=': '_match_le',
  257. '>=': '_match_ge',
  258. '==': '_match_eq',
  259. '===': '_match_arbitrary',
  260. '!=': '_match_ne',
  261. }
  262. def _adjust_local(self, version, constraint, prefix):
  263. if prefix:
  264. strip_local = '+' not in constraint and version._parts[-1]
  265. else:
  266. # both constraint and version are
  267. # NormalizedVersion instances.
  268. # If constraint does not have a local component,
  269. # ensure the version doesn't, either.
  270. strip_local = not constraint._parts[-1] and version._parts[-1]
  271. if strip_local:
  272. s = version._string.split('+', 1)[0]
  273. version = self.version_class(s)
  274. return version, constraint
  275. def _match_lt(self, version, constraint, prefix):
  276. version, constraint = self._adjust_local(version, constraint, prefix)
  277. if version >= constraint:
  278. return False
  279. release_clause = constraint._release_clause
  280. pfx = '.'.join([str(i) for i in release_clause])
  281. return not _match_prefix(version, pfx)
  282. def _match_gt(self, version, constraint, prefix):
  283. version, constraint = self._adjust_local(version, constraint, prefix)
  284. if version <= constraint:
  285. return False
  286. release_clause = constraint._release_clause
  287. pfx = '.'.join([str(i) for i in release_clause])
  288. return not _match_prefix(version, pfx)
  289. def _match_le(self, version, constraint, prefix):
  290. version, constraint = self._adjust_local(version, constraint, prefix)
  291. return version <= constraint
  292. def _match_ge(self, version, constraint, prefix):
  293. version, constraint = self._adjust_local(version, constraint, prefix)
  294. return version >= constraint
  295. def _match_eq(self, version, constraint, prefix):
  296. version, constraint = self._adjust_local(version, constraint, prefix)
  297. if not prefix:
  298. result = (version == constraint)
  299. else:
  300. result = _match_prefix(version, constraint)
  301. return result
  302. def _match_arbitrary(self, version, constraint, prefix):
  303. return str(version) == str(constraint)
  304. def _match_ne(self, version, constraint, prefix):
  305. version, constraint = self._adjust_local(version, constraint, prefix)
  306. if not prefix:
  307. result = (version != constraint)
  308. else:
  309. result = not _match_prefix(version, constraint)
  310. return result
  311. def _match_compatible(self, version, constraint, prefix):
  312. version, constraint = self._adjust_local(version, constraint, prefix)
  313. if version == constraint:
  314. return True
  315. if version < constraint:
  316. return False
  317. # if not prefix:
  318. # return True
  319. release_clause = constraint._release_clause
  320. if len(release_clause) > 1:
  321. release_clause = release_clause[:-1]
  322. pfx = '.'.join([str(i) for i in release_clause])
  323. return _match_prefix(version, pfx)
  324. _REPLACEMENTS = (
  325. (re.compile('[.+-]$'), ''), # remove trailing puncts
  326. (re.compile(r'^[.](\d)'), r'0.\1'), # .N -> 0.N at start
  327. (re.compile('^[.-]'), ''), # remove leading puncts
  328. (re.compile(r'^\((.*)\)$'), r'\1'), # remove parentheses
  329. (re.compile(r'^v(ersion)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
  330. (re.compile(r'^r(ev)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
  331. (re.compile('[.]{2,}'), '.'), # multiple runs of '.'
  332. (re.compile(r'\b(alfa|apha)\b'), 'alpha'), # misspelt alpha
  333. (re.compile(r'\b(pre-alpha|prealpha)\b'),
  334. 'pre.alpha'), # standardise
  335. (re.compile(r'\(beta\)$'), 'beta'), # remove parentheses
  336. )
  337. _SUFFIX_REPLACEMENTS = (
  338. (re.compile('^[:~._+-]+'), ''), # remove leading puncts
  339. (re.compile('[,*")([\]]'), ''), # remove unwanted chars
  340. (re.compile('[~:+_ -]'), '.'), # replace illegal chars
  341. (re.compile('[.]{2,}'), '.'), # multiple runs of '.'
  342. (re.compile(r'\.$'), ''), # trailing '.'
  343. )
  344. _NUMERIC_PREFIX = re.compile(r'(\d+(\.\d+)*)')
  345. def _suggest_semantic_version(s):
  346. """
  347. Try to suggest a semantic form for a version for which
  348. _suggest_normalized_version couldn't come up with anything.
  349. """
  350. result = s.strip().lower()
  351. for pat, repl in _REPLACEMENTS:
  352. result = pat.sub(repl, result)
  353. if not result:
  354. result = '0.0.0'
  355. # Now look for numeric prefix, and separate it out from
  356. # the rest.
  357. #import pdb; pdb.set_trace()
  358. m = _NUMERIC_PREFIX.match(result)
  359. if not m:
  360. prefix = '0.0.0'
  361. suffix = result
  362. else:
  363. prefix = m.groups()[0].split('.')
  364. prefix = [int(i) for i in prefix]
  365. while len(prefix) < 3:
  366. prefix.append(0)
  367. if len(prefix) == 3:
  368. suffix = result[m.end():]
  369. else:
  370. suffix = '.'.join([str(i) for i in prefix[3:]]) + result[m.end():]
  371. prefix = prefix[:3]
  372. prefix = '.'.join([str(i) for i in prefix])
  373. suffix = suffix.strip()
  374. if suffix:
  375. #import pdb; pdb.set_trace()
  376. # massage the suffix.
  377. for pat, repl in _SUFFIX_REPLACEMENTS:
  378. suffix = pat.sub(repl, suffix)
  379. if not suffix:
  380. result = prefix
  381. else:
  382. sep = '-' if 'dev' in suffix else '+'
  383. result = prefix + sep + suffix
  384. if not is_semver(result):
  385. result = None
  386. return result
  387. def _suggest_normalized_version(s):
  388. """Suggest a normalized version close to the given version string.
  389. If you have a version string that isn't rational (i.e. NormalizedVersion
  390. doesn't like it) then you might be able to get an equivalent (or close)
  391. rational version from this function.
  392. This does a number of simple normalizations to the given string, based
  393. on observation of versions currently in use on PyPI. Given a dump of
  394. those version during PyCon 2009, 4287 of them:
  395. - 2312 (53.93%) match NormalizedVersion without change
  396. with the automatic suggestion
  397. - 3474 (81.04%) match when using this suggestion method
  398. @param s {str} An irrational version string.
  399. @returns A rational version string, or None, if couldn't determine one.
  400. """
  401. try:
  402. _normalized_key(s)
  403. return s # already rational
  404. except UnsupportedVersionError:
  405. pass
  406. rs = s.lower()
  407. # part of this could use maketrans
  408. for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
  409. ('beta', 'b'), ('rc', 'c'), ('-final', ''),
  410. ('-pre', 'c'),
  411. ('-release', ''), ('.release', ''), ('-stable', ''),
  412. ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
  413. ('final', '')):
  414. rs = rs.replace(orig, repl)
  415. # if something ends with dev or pre, we add a 0
  416. rs = re.sub(r"pre$", r"pre0", rs)
  417. rs = re.sub(r"dev$", r"dev0", rs)
  418. # if we have something like "b-2" or "a.2" at the end of the
  419. # version, that is probably beta, alpha, etc
  420. # let's remove the dash or dot
  421. rs = re.sub(r"([abc]|rc)[\-\.](\d+)$", r"\1\2", rs)
  422. # 1.0-dev-r371 -> 1.0.dev371
  423. # 0.1-dev-r79 -> 0.1.dev79
  424. rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
  425. # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
  426. rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
  427. # Clean: v0.3, v1.0
  428. if rs.startswith('v'):
  429. rs = rs[1:]
  430. # Clean leading '0's on numbers.
  431. #TODO: unintended side-effect on, e.g., "2003.05.09"
  432. # PyPI stats: 77 (~2%) better
  433. rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
  434. # Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers
  435. # zero.
  436. # PyPI stats: 245 (7.56%) better
  437. rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
  438. # the 'dev-rNNN' tag is a dev tag
  439. rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
  440. # clean the - when used as a pre delimiter
  441. rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
  442. # a terminal "dev" or "devel" can be changed into ".dev0"
  443. rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
  444. # a terminal "dev" can be changed into ".dev0"
  445. rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
  446. # a terminal "final" or "stable" can be removed
  447. rs = re.sub(r"(final|stable)$", "", rs)
  448. # The 'r' and the '-' tags are post release tags
  449. # 0.4a1.r10 -> 0.4a1.post10
  450. # 0.9.33-17222 -> 0.9.33.post17222
  451. # 0.9.33-r17222 -> 0.9.33.post17222
  452. rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
  453. # Clean 'r' instead of 'dev' usage:
  454. # 0.9.33+r17222 -> 0.9.33.dev17222
  455. # 1.0dev123 -> 1.0.dev123
  456. # 1.0.git123 -> 1.0.dev123
  457. # 1.0.bzr123 -> 1.0.dev123
  458. # 0.1a0dev.123 -> 0.1a0.dev123
  459. # PyPI stats: ~150 (~4%) better
  460. rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
  461. # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
  462. # 0.2.pre1 -> 0.2c1
  463. # 0.2-c1 -> 0.2c1
  464. # 1.0preview123 -> 1.0c123
  465. # PyPI stats: ~21 (0.62%) better
  466. rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
  467. # Tcl/Tk uses "px" for their post release markers
  468. rs = re.sub(r"p(\d+)$", r".post\1", rs)
  469. try:
  470. _normalized_key(rs)
  471. except UnsupportedVersionError:
  472. rs = None
  473. return rs
  474. #
  475. # Legacy version processing (distribute-compatible)
  476. #
  477. _VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I)
  478. _VERSION_REPLACE = {
  479. 'pre': 'c',
  480. 'preview': 'c',
  481. '-': 'final-',
  482. 'rc': 'c',
  483. 'dev': '@',
  484. '': None,
  485. '.': None,
  486. }
  487. def _legacy_key(s):
  488. def get_parts(s):
  489. result = []
  490. for p in _VERSION_PART.split(s.lower()):
  491. p = _VERSION_REPLACE.get(p, p)
  492. if p:
  493. if '0' <= p[:1] <= '9':
  494. p = p.zfill(8)
  495. else:
  496. p = '*' + p
  497. result.append(p)
  498. result.append('*final')
  499. return result
  500. result = []
  501. for p in get_parts(s):
  502. if p.startswith('*'):
  503. if p < '*final':
  504. while result and result[-1] == '*final-':
  505. result.pop()
  506. while result and result[-1] == '00000000':
  507. result.pop()
  508. result.append(p)
  509. return tuple(result)
  510. class LegacyVersion(Version):
  511. def parse(self, s):
  512. return _legacy_key(s)
  513. @property
  514. def is_prerelease(self):
  515. result = False
  516. for x in self._parts:
  517. if (isinstance(x, string_types) and x.startswith('*') and
  518. x < '*final'):
  519. result = True
  520. break
  521. return result
  522. class LegacyMatcher(Matcher):
  523. version_class = LegacyVersion
  524. _operators = dict(Matcher._operators)
  525. _operators['~='] = '_match_compatible'
  526. numeric_re = re.compile('^(\d+(\.\d+)*)')
  527. def _match_compatible(self, version, constraint, prefix):
  528. if version < constraint:
  529. return False
  530. m = self.numeric_re.match(str(constraint))
  531. if not m:
  532. logger.warning('Cannot compute compatible match for version %s '
  533. ' and constraint %s', version, constraint)
  534. return True
  535. s = m.groups()[0]
  536. if '.' in s:
  537. s = s.rsplit('.', 1)[0]
  538. return _match_prefix(version, s)
  539. #
  540. # Semantic versioning
  541. #
  542. _SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)'
  543. r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?'
  544. r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I)
  545. def is_semver(s):
  546. return _SEMVER_RE.match(s)
  547. def _semantic_key(s):
  548. def make_tuple(s, absent):
  549. if s is None:
  550. result = (absent,)
  551. else:
  552. parts = s[1:].split('.')
  553. # We can't compare ints and strings on Python 3, so fudge it
  554. # by zero-filling numeric values so simulate a numeric comparison
  555. result = tuple([p.zfill(8) if p.isdigit() else p for p in parts])
  556. return result
  557. m = is_semver(s)
  558. if not m:
  559. raise UnsupportedVersionError(s)
  560. groups = m.groups()
  561. major, minor, patch = [int(i) for i in groups[:3]]
  562. # choose the '|' and '*' so that versions sort correctly
  563. pre, build = make_tuple(groups[3], '|'), make_tuple(groups[5], '*')
  564. return (major, minor, patch), pre, build
  565. class SemanticVersion(Version):
  566. def parse(self, s):
  567. return _semantic_key(s)
  568. @property
  569. def is_prerelease(self):
  570. return self._parts[1][0] != '|'
  571. class SemanticMatcher(Matcher):
  572. version_class = SemanticVersion
  573. class VersionScheme(object):
  574. def __init__(self, key, matcher, suggester=None):
  575. self.key = key
  576. self.matcher = matcher
  577. self.suggester = suggester
  578. def is_valid_version(self, s):
  579. try:
  580. self.matcher.version_class(s)
  581. result = True
  582. except UnsupportedVersionError:
  583. result = False
  584. return result
  585. def is_valid_matcher(self, s):
  586. try:
  587. self.matcher(s)
  588. result = True
  589. except UnsupportedVersionError:
  590. result = False
  591. return result
  592. def is_valid_constraint_list(self, s):
  593. """
  594. Used for processing some metadata fields
  595. """
  596. return self.is_valid_matcher('dummy_name (%s)' % s)
  597. def suggest(self, s):
  598. if self.suggester is None:
  599. result = None
  600. else:
  601. result = self.suggester(s)
  602. return result
  603. _SCHEMES = {
  604. 'normalized': VersionScheme(_normalized_key, NormalizedMatcher,
  605. _suggest_normalized_version),
  606. 'legacy': VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s),
  607. 'semantic': VersionScheme(_semantic_key, SemanticMatcher,
  608. _suggest_semantic_version),
  609. }
  610. _SCHEMES['default'] = _SCHEMES['normalized']
  611. def get_scheme(name):
  612. if name not in _SCHEMES:
  613. raise ValueError('unknown scheme name: %r' % name)
  614. return _SCHEMES[name]