PageRenderTime 55ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/pip/_vendor/distlib/version.py

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