123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736 |
- """
- Implementation of a flexible versioning scheme providing support for PEP-440,
- setuptools-compatible and semantic versioning.
- """
- import logging
- import re
- from .compat import string_types
- from .util import parse_requirement
- __all__ = ['NormalizedVersion', 'NormalizedMatcher',
- 'LegacyVersion', 'LegacyMatcher',
- 'SemanticVersion', 'SemanticMatcher',
- 'UnsupportedVersionError', 'get_scheme']
- logger = logging.getLogger(__name__)
- class UnsupportedVersionError(ValueError):
- """This is an unsupported version."""
- pass
- class Version(object):
- def __init__(self, s):
- self._string = s = s.strip()
- self._parts = parts = self.parse(s)
- assert isinstance(parts, tuple)
- assert len(parts) > 0
- def parse(self, s):
- raise NotImplementedError('please implement in a subclass')
- def _check_compatible(self, other):
- if type(self) != type(other):
- raise TypeError('cannot compare %r and %r' % (self, other))
- def __eq__(self, other):
- self._check_compatible(other)
- return self._parts == other._parts
- def __ne__(self, other):
- return not self.__eq__(other)
- def __lt__(self, other):
- self._check_compatible(other)
- return self._parts < other._parts
- def __gt__(self, other):
- return not (self.__lt__(other) or self.__eq__(other))
- def __le__(self, other):
- return self.__lt__(other) or self.__eq__(other)
- def __ge__(self, other):
- return self.__gt__(other) or self.__eq__(other)
-
- def __hash__(self):
- return hash(self._parts)
- def __repr__(self):
- return "%s('%s')" % (self.__class__.__name__, self._string)
- def __str__(self):
- return self._string
- @property
- def is_prerelease(self):
- raise NotImplementedError('Please implement in subclasses.')
- class Matcher(object):
- version_class = None
-
- _operators = {
- '<': lambda v, c, p: v < c,
- '>': lambda v, c, p: v > c,
- '<=': lambda v, c, p: v == c or v < c,
- '>=': lambda v, c, p: v == c or v > c,
- '==': lambda v, c, p: v == c,
- '===': lambda v, c, p: v == c,
-
- '~=': lambda v, c, p: v == c or v > c,
- '!=': lambda v, c, p: v != c,
- }
-
-
- def parse_requirement(self, s):
- return parse_requirement(s)
- def __init__(self, s):
- if self.version_class is None:
- raise ValueError('Please specify a version class')
- self._string = s = s.strip()
- r = self.parse_requirement(s)
- if not r:
- raise ValueError('Not valid: %r' % s)
- self.name = r.name
- self.key = self.name.lower()
- clist = []
- if r.constraints:
-
- for op, s in r.constraints:
- if s.endswith('.*'):
- if op not in ('==', '!='):
- raise ValueError('\'.*\' not allowed for '
- '%r constraints' % op)
-
-
- vn, prefix = s[:-2], True
-
- self.version_class(vn)
- else:
-
-
- vn, prefix = self.version_class(s), False
- clist.append((op, vn, prefix))
- self._parts = tuple(clist)
- def match(self, version):
- """
- Check if the provided version matches the constraints.
- :param version: The version to match against this instance.
- :type version: String or :class:`Version` instance.
- """
- if isinstance(version, string_types):
- version = self.version_class(version)
- for operator, constraint, prefix in self._parts:
- f = self._operators.get(operator)
- if isinstance(f, string_types):
- f = getattr(self, f)
- if not f:
- msg = ('%r not implemented '
- 'for %s' % (operator, self.__class__.__name__))
- raise NotImplementedError(msg)
- if not f(version, constraint, prefix):
- return False
- return True
- @property
- def exact_version(self):
- result = None
- if len(self._parts) == 1 and self._parts[0][0] in ('==', '==='):
- result = self._parts[0][1]
- return result
- def _check_compatible(self, other):
- if type(self) != type(other) or self.name != other.name:
- raise TypeError('cannot compare %s and %s' % (self, other))
- def __eq__(self, other):
- self._check_compatible(other)
- return self.key == other.key and self._parts == other._parts
- def __ne__(self, other):
- return not self.__eq__(other)
-
- def __hash__(self):
- return hash(self.key) + hash(self._parts)
- def __repr__(self):
- return "%s(%r)" % (self.__class__.__name__, self._string)
- def __str__(self):
- return self._string
- PEP440_VERSION_RE = re.compile(r'^v?(\d+!)?(\d+(\.\d+)*)((a|b|c|rc)(\d+))?'
- r'(\.(post)(\d+))?(\.(dev)(\d+))?'
- r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$')
- def _pep_440_key(s):
- s = s.strip()
- m = PEP440_VERSION_RE.match(s)
- if not m:
- raise UnsupportedVersionError('Not a valid version: %s' % s)
- groups = m.groups()
- nums = tuple(int(v) for v in groups[1].split('.'))
- while len(nums) > 1 and nums[-1] == 0:
- nums = nums[:-1]
- if not groups[0]:
- epoch = 0
- else:
- epoch = int(groups[0])
- pre = groups[4:6]
- post = groups[7:9]
- dev = groups[10:12]
- local = groups[13]
- if pre == (None, None):
- pre = ()
- else:
- pre = pre[0], int(pre[1])
- if post == (None, None):
- post = ()
- else:
- post = post[0], int(post[1])
- if dev == (None, None):
- dev = ()
- else:
- dev = dev[0], int(dev[1])
- if local is None:
- local = ()
- else:
- parts = []
- for part in local.split('.'):
-
-
-
- if part.isdigit():
- part = (1, int(part))
- else:
- part = (0, part)
- parts.append(part)
- local = tuple(parts)
- if not pre:
-
- if not post and dev:
-
- pre = ('a', -1)
- else:
- pre = ('z',)
-
- if not post:
- post = ('_',)
- if not dev:
- dev = ('final',)
-
- return epoch, nums, pre, post, dev, local
- _normalized_key = _pep_440_key
- class NormalizedVersion(Version):
- """A rational version.
- Good:
- 1.2 # equivalent to "1.2.0"
- 1.2.0
- 1.2a1
- 1.2.3a2
- 1.2.3b1
- 1.2.3c1
- 1.2.3.4
- TODO: fill this out
- Bad:
- 1 # minimum two numbers
- 1.2a # release level must have a release serial
- 1.2.3b
- """
- def parse(self, s):
- result = _normalized_key(s)
-
-
-
-
- m = PEP440_VERSION_RE.match(s)
- groups = m.groups()
- self._release_clause = tuple(int(v) for v in groups[1].split('.'))
- return result
- PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev'])
- @property
- def is_prerelease(self):
- return any(t[0] in self.PREREL_TAGS for t in self._parts if t)
- def _match_prefix(x, y):
- x = str(x)
- y = str(y)
- if x == y:
- return True
- if not x.startswith(y):
- return False
- n = len(y)
- return x[n] == '.'
- class NormalizedMatcher(Matcher):
- version_class = NormalizedVersion
-
- _operators = {
- '~=': '_match_compatible',
- '<': '_match_lt',
- '>': '_match_gt',
- '<=': '_match_le',
- '>=': '_match_ge',
- '==': '_match_eq',
- '===': '_match_arbitrary',
- '!=': '_match_ne',
- }
- def _adjust_local(self, version, constraint, prefix):
- if prefix:
- strip_local = '+' not in constraint and version._parts[-1]
- else:
-
-
-
-
- strip_local = not constraint._parts[-1] and version._parts[-1]
- if strip_local:
- s = version._string.split('+', 1)[0]
- version = self.version_class(s)
- return version, constraint
- def _match_lt(self, version, constraint, prefix):
- version, constraint = self._adjust_local(version, constraint, prefix)
- if version >= constraint:
- return False
- release_clause = constraint._release_clause
- pfx = '.'.join([str(i) for i in release_clause])
- return not _match_prefix(version, pfx)
- def _match_gt(self, version, constraint, prefix):
- version, constraint = self._adjust_local(version, constraint, prefix)
- if version <= constraint:
- return False
- release_clause = constraint._release_clause
- pfx = '.'.join([str(i) for i in release_clause])
- return not _match_prefix(version, pfx)
- def _match_le(self, version, constraint, prefix):
- version, constraint = self._adjust_local(version, constraint, prefix)
- return version <= constraint
- def _match_ge(self, version, constraint, prefix):
- version, constraint = self._adjust_local(version, constraint, prefix)
- return version >= constraint
- def _match_eq(self, version, constraint, prefix):
- version, constraint = self._adjust_local(version, constraint, prefix)
- if not prefix:
- result = (version == constraint)
- else:
- result = _match_prefix(version, constraint)
- return result
- def _match_arbitrary(self, version, constraint, prefix):
- return str(version) == str(constraint)
- def _match_ne(self, version, constraint, prefix):
- version, constraint = self._adjust_local(version, constraint, prefix)
- if not prefix:
- result = (version != constraint)
- else:
- result = not _match_prefix(version, constraint)
- return result
- def _match_compatible(self, version, constraint, prefix):
- version, constraint = self._adjust_local(version, constraint, prefix)
- if version == constraint:
- return True
- if version < constraint:
- return False
- release_clause = constraint._release_clause
- if len(release_clause) > 1:
- release_clause = release_clause[:-1]
- pfx = '.'.join([str(i) for i in release_clause])
- return _match_prefix(version, pfx)
- _REPLACEMENTS = (
- (re.compile('[.+-]$'), ''),
- (re.compile(r'^[.](\d)'), r'0.\1'),
- (re.compile('^[.-]'), ''),
- (re.compile(r'^\((.*)\)$'), r'\1'),
- (re.compile(r'^v(ersion)?\s*(\d+)'), r'\2'),
- (re.compile(r'^r(ev)?\s*(\d+)'), r'\2'),
- (re.compile('[.]{2,}'), '.'),
- (re.compile(r'\b(alfa|apha)\b'), 'alpha'),
- (re.compile(r'\b(pre-alpha|prealpha)\b'),
- 'pre.alpha'),
- (re.compile(r'\(beta\)$'), 'beta'),
- )
- _SUFFIX_REPLACEMENTS = (
- (re.compile('^[:~._+-]+'), ''),
- (re.compile('[,*")([\\]]'), ''),
- (re.compile('[~:+_ -]'), '.'),
- (re.compile('[.]{2,}'), '.'),
- (re.compile(r'\.$'), ''),
- )
- _NUMERIC_PREFIX = re.compile(r'(\d+(\.\d+)*)')
- def _suggest_semantic_version(s):
- """
- Try to suggest a semantic form for a version for which
- _suggest_normalized_version couldn't come up with anything.
- """
- result = s.strip().lower()
- for pat, repl in _REPLACEMENTS:
- result = pat.sub(repl, result)
- if not result:
- result = '0.0.0'
-
-
-
- m = _NUMERIC_PREFIX.match(result)
- if not m:
- prefix = '0.0.0'
- suffix = result
- else:
- prefix = m.groups()[0].split('.')
- prefix = [int(i) for i in prefix]
- while len(prefix) < 3:
- prefix.append(0)
- if len(prefix) == 3:
- suffix = result[m.end():]
- else:
- suffix = '.'.join([str(i) for i in prefix[3:]]) + result[m.end():]
- prefix = prefix[:3]
- prefix = '.'.join([str(i) for i in prefix])
- suffix = suffix.strip()
- if suffix:
-
-
- for pat, repl in _SUFFIX_REPLACEMENTS:
- suffix = pat.sub(repl, suffix)
- if not suffix:
- result = prefix
- else:
- sep = '-' if 'dev' in suffix else '+'
- result = prefix + sep + suffix
- if not is_semver(result):
- result = None
- return result
- def _suggest_normalized_version(s):
- """Suggest a normalized version close to the given version string.
- If you have a version string that isn't rational (i.e. NormalizedVersion
- doesn't like it) then you might be able to get an equivalent (or close)
- rational version from this function.
- This does a number of simple normalizations to the given string, based
- on observation of versions currently in use on PyPI. Given a dump of
- those version during PyCon 2009, 4287 of them:
- - 2312 (53.93%) match NormalizedVersion without change
- with the automatic suggestion
- - 3474 (81.04%) match when using this suggestion method
- @param s {str} An irrational version string.
- @returns A rational version string, or None, if couldn't determine one.
- """
- try:
- _normalized_key(s)
- return s
- except UnsupportedVersionError:
- pass
- rs = s.lower()
-
- for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
- ('beta', 'b'), ('rc', 'c'), ('-final', ''),
- ('-pre', 'c'),
- ('-release', ''), ('.release', ''), ('-stable', ''),
- ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
- ('final', '')):
- rs = rs.replace(orig, repl)
-
- rs = re.sub(r"pre$", r"pre0", rs)
- rs = re.sub(r"dev$", r"dev0", rs)
-
-
-
- rs = re.sub(r"([abc]|rc)[\-\.](\d+)$", r"\1\2", rs)
-
-
- rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
-
- rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
-
- if rs.startswith('v'):
- rs = rs[1:]
-
-
-
- rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
-
-
-
- rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
-
- rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
-
- rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
-
- rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
-
- rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
-
- rs = re.sub(r"(final|stable)$", "", rs)
-
-
-
-
- rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
-
-
-
-
-
-
-
- rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
-
-
-
-
-
- rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
-
- rs = re.sub(r"p(\d+)$", r".post\1", rs)
- try:
- _normalized_key(rs)
- except UnsupportedVersionError:
- rs = None
- return rs
- _VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I)
- _VERSION_REPLACE = {
- 'pre': 'c',
- 'preview': 'c',
- '-': 'final-',
- 'rc': 'c',
- 'dev': '@',
- '': None,
- '.': None,
- }
- def _legacy_key(s):
- def get_parts(s):
- result = []
- for p in _VERSION_PART.split(s.lower()):
- p = _VERSION_REPLACE.get(p, p)
- if p:
- if '0' <= p[:1] <= '9':
- p = p.zfill(8)
- else:
- p = '*' + p
- result.append(p)
- result.append('*final')
- return result
- result = []
- for p in get_parts(s):
- if p.startswith('*'):
- if p < '*final':
- while result and result[-1] == '*final-':
- result.pop()
- while result and result[-1] == '00000000':
- result.pop()
- result.append(p)
- return tuple(result)
- class LegacyVersion(Version):
- def parse(self, s):
- return _legacy_key(s)
- @property
- def is_prerelease(self):
- result = False
- for x in self._parts:
- if (isinstance(x, string_types) and x.startswith('*') and
- x < '*final'):
- result = True
- break
- return result
- class LegacyMatcher(Matcher):
- version_class = LegacyVersion
- _operators = dict(Matcher._operators)
- _operators['~='] = '_match_compatible'
- numeric_re = re.compile(r'^(\d+(\.\d+)*)')
- def _match_compatible(self, version, constraint, prefix):
- if version < constraint:
- return False
- m = self.numeric_re.match(str(constraint))
- if not m:
- logger.warning('Cannot compute compatible match for version %s '
- ' and constraint %s', version, constraint)
- return True
- s = m.groups()[0]
- if '.' in s:
- s = s.rsplit('.', 1)[0]
- return _match_prefix(version, s)
- _SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)'
- r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?'
- r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I)
- def is_semver(s):
- return _SEMVER_RE.match(s)
- def _semantic_key(s):
- def make_tuple(s, absent):
- if s is None:
- result = (absent,)
- else:
- parts = s[1:].split('.')
-
-
- result = tuple([p.zfill(8) if p.isdigit() else p for p in parts])
- return result
- m = is_semver(s)
- if not m:
- raise UnsupportedVersionError(s)
- groups = m.groups()
- major, minor, patch = [int(i) for i in groups[:3]]
-
- pre, build = make_tuple(groups[3], '|'), make_tuple(groups[5], '*')
- return (major, minor, patch), pre, build
- class SemanticVersion(Version):
- def parse(self, s):
- return _semantic_key(s)
- @property
- def is_prerelease(self):
- return self._parts[1][0] != '|'
- class SemanticMatcher(Matcher):
- version_class = SemanticVersion
- class VersionScheme(object):
- def __init__(self, key, matcher, suggester=None):
- self.key = key
- self.matcher = matcher
- self.suggester = suggester
- def is_valid_version(self, s):
- try:
- self.matcher.version_class(s)
- result = True
- except UnsupportedVersionError:
- result = False
- return result
- def is_valid_matcher(self, s):
- try:
- self.matcher(s)
- result = True
- except UnsupportedVersionError:
- result = False
- return result
- def is_valid_constraint_list(self, s):
- """
- Used for processing some metadata fields
- """
- return self.is_valid_matcher('dummy_name (%s)' % s)
- def suggest(self, s):
- if self.suggester is None:
- result = None
- else:
- result = self.suggester(s)
- return result
- _SCHEMES = {
- 'normalized': VersionScheme(_normalized_key, NormalizedMatcher,
- _suggest_normalized_version),
- 'legacy': VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s),
- 'semantic': VersionScheme(_semantic_key, SemanticMatcher,
- _suggest_semantic_version),
- }
- _SCHEMES['default'] = _SCHEMES['normalized']
- def get_scheme(name):
- if name not in _SCHEMES:
- raise ValueError('unknown scheme name: %r' % name)
- return _SCHEMES[name]
|