Development of an internal social media platform with personalised dashboards for students
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.

version.py 23KB

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