1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2012-2016 The Python Software Foundation.
4
# See LICENSE.txt and CONTRIBUTORS.txt.
7
Implementation of a flexible versioning scheme providing support for PEP-440,
8
setuptools-compatible and semantic versioning.
14
from .compat import string_types
16
__all__ = ['NormalizedVersion', 'NormalizedMatcher',
17
'LegacyVersion', 'LegacyMatcher',
18
'SemanticVersion', 'SemanticMatcher',
19
'UnsupportedVersionError', 'get_scheme']
21
logger = logging.getLogger(__name__)
24
class UnsupportedVersionError(ValueError):
25
"""This is an unsupported version."""
29
class Version(object):
30
def __init__(self, s):
31
self._string = s = s.strip()
32
self._parts = parts = self.parse(s)
33
assert isinstance(parts, tuple)
37
raise NotImplementedError('please implement in a subclass')
39
def _check_compatible(self, other):
40
if type(self) != type(other):
41
raise TypeError('cannot compare %r and %r' % (self, other))
43
def __eq__(self, other):
44
self._check_compatible(other)
45
return self._parts == other._parts
47
def __ne__(self, other):
48
return not self.__eq__(other)
50
def __lt__(self, other):
51
self._check_compatible(other)
52
return self._parts < other._parts
54
def __gt__(self, other):
55
return not (self.__lt__(other) or self.__eq__(other))
57
def __le__(self, other):
58
return self.__lt__(other) or self.__eq__(other)
60
def __ge__(self, other):
61
return self.__gt__(other) or self.__eq__(other)
63
# See http://docs.python.org/reference/datamodel#object.__hash__
65
return hash(self._parts)
68
return "%s('%s')" % (self.__class__.__name__, self._string)
74
def is_prerelease(self):
75
raise NotImplementedError('Please implement in subclasses.')
78
class Matcher(object):
81
dist_re = re.compile(r"^(\w[\s\w'.-]*)(\((.*)\))?")
82
comp_re = re.compile(r'^(<=|>=|<|>|!=|={2,3}|~=)?\s*([^\s,]+)$')
83
num_re = re.compile(r'^\d+(\.\d+)*$')
85
# value is either a callable or the name of a method
87
'<': lambda v, c, p: v < c,
88
'>': lambda v, c, p: v > c,
89
'<=': lambda v, c, p: v == c or v < c,
90
'>=': lambda v, c, p: v == c or v > c,
91
'==': lambda v, c, p: v == c,
92
'===': lambda v, c, p: v == c,
93
# by default, compatible => >=.
94
'~=': lambda v, c, p: v == c or v > c,
95
'!=': lambda v, c, p: v != c,
98
def __init__(self, s):
99
if self.version_class is None:
100
raise ValueError('Please specify a version class')
101
self._string = s = s.strip()
102
m = self.dist_re.match(s)
104
raise ValueError('Not valid: %r' % s)
105
groups = m.groups('')
106
self.name = groups[0].strip()
107
self.key = self.name.lower() # for case-insensitive comparisons
110
constraints = [c.strip() for c in groups[2].split(',')]
111
for c in constraints:
112
m = self.comp_re.match(c)
114
raise ValueError('Invalid %r in %r' % (c, s))
116
op = groups[0] or '~='
119
if op not in ('==', '!='):
120
raise ValueError('\'.*\' not allowed for '
121
'%r constraints' % op)
122
# Could be a partial version (e.g. for '2.*') which
123
# won't parse as a version, so keep it as a string
124
vn, prefix = s[:-2], True
125
if not self.num_re.match(vn):
126
# Just to check that vn is a valid version
127
self.version_class(vn)
129
# Should parse as a version, so we can create an
130
# instance for the comparison
131
vn, prefix = self.version_class(s), False
132
clist.append((op, vn, prefix))
133
self._parts = tuple(clist)
135
def match(self, version):
137
Check if the provided version matches the constraints.
139
:param version: The version to match against this instance.
140
:type version: Strring or :class:`Version` instance.
142
if isinstance(version, string_types):
143
version = self.version_class(version)
144
for operator, constraint, prefix in self._parts:
145
f = self._operators.get(operator)
146
if isinstance(f, string_types):
149
msg = ('%r not implemented '
150
'for %s' % (operator, self.__class__.__name__))
151
raise NotImplementedError(msg)
152
if not f(version, constraint, prefix):
157
def exact_version(self):
159
if len(self._parts) == 1 and self._parts[0][0] in ('==', '==='):
160
result = self._parts[0][1]
163
def _check_compatible(self, other):
164
if type(self) != type(other) or self.name != other.name:
165
raise TypeError('cannot compare %s and %s' % (self, other))
167
def __eq__(self, other):
168
self._check_compatible(other)
169
return self.key == other.key and self._parts == other._parts
171
def __ne__(self, other):
172
return not self.__eq__(other)
174
# See http://docs.python.org/reference/datamodel#object.__hash__
176
return hash(self.key) + hash(self._parts)
179
return "%s(%r)" % (self.__class__.__name__, self._string)
185
PEP440_VERSION_RE = re.compile(r'^v?(\d+!)?(\d+(\.\d+)*)((a|b|c|rc)(\d+))?'
186
r'(\.(post)(\d+))?(\.(dev)(\d+))?'
187
r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$')
192
m = PEP440_VERSION_RE.match(s)
194
raise UnsupportedVersionError('Not a valid version: %s' % s)
196
nums = tuple(int(v) for v in groups[1].split('.'))
197
while len(nums) > 1 and nums[-1] == 0:
203
epoch = int(groups[0])
208
if pre == (None, None):
211
pre = pre[0], int(pre[1])
212
if post == (None, None):
215
post = post[0], int(post[1])
216
if dev == (None, None):
219
dev = dev[0], int(dev[1])
224
for part in local.split('.'):
225
# to ensure that numeric compares as > lexicographic, avoid
226
# comparing them directly, but encode a tuple which ensures
229
part = (1, int(part))
235
# either before pre-release, or final release and after
238
pre = ('a', -1) # to sort before a0
240
pre = ('z',) # to sort after all pre-releases
241
# now look at the state of post and dev.
243
post = ('_',) # sort before 'a'
247
#print('%s -> %s' % (s, m.groups()))
248
return epoch, nums, pre, post, dev, local
251
_normalized_key = _pep_440_key
254
class NormalizedVersion(Version):
255
"""A rational version.
258
1.2 # equivalent to "1.2.0"
268
1 # mininum two numbers
269
1.2a # release level must have a release serial
273
result = _normalized_key(s)
274
# _normalized_key loses trailing zeroes in the release
275
# clause, since that's needed to ensure that X.Y == X.Y.0 == X.Y.0.0
276
# However, PEP 440 prefix matching needs it: for example,
277
# (~= 1.4.5.0) matches differently to (~= 1.4.5.0.0).
278
m = PEP440_VERSION_RE.match(s) # must succeed
280
self._release_clause = tuple(int(v) for v in groups[1].split('.'))
283
PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev'])
286
def is_prerelease(self):
287
return any(t[0] in self.PREREL_TAGS for t in self._parts if t)
290
def _match_prefix(x, y):
295
if not x.startswith(y):
301
class NormalizedMatcher(Matcher):
302
version_class = NormalizedVersion
304
# value is either a callable or the name of a method
306
'~=': '_match_compatible',
312
'===': '_match_arbitrary',
316
def _adjust_local(self, version, constraint, prefix):
318
strip_local = '+' not in constraint and version._parts[-1]
320
# both constraint and version are
321
# NormalizedVersion instances.
322
# If constraint does not have a local component,
323
# ensure the version doesn't, either.
324
strip_local = not constraint._parts[-1] and version._parts[-1]
326
s = version._string.split('+', 1)[0]
327
version = self.version_class(s)
328
return version, constraint
330
def _match_lt(self, version, constraint, prefix):
331
version, constraint = self._adjust_local(version, constraint, prefix)
332
if version >= constraint:
334
release_clause = constraint._release_clause
335
pfx = '.'.join([str(i) for i in release_clause])
336
return not _match_prefix(version, pfx)
338
def _match_gt(self, version, constraint, prefix):
339
version, constraint = self._adjust_local(version, constraint, prefix)
340
if version <= constraint:
342
release_clause = constraint._release_clause
343
pfx = '.'.join([str(i) for i in release_clause])
344
return not _match_prefix(version, pfx)
346
def _match_le(self, version, constraint, prefix):
347
version, constraint = self._adjust_local(version, constraint, prefix)
348
return version <= constraint
350
def _match_ge(self, version, constraint, prefix):
351
version, constraint = self._adjust_local(version, constraint, prefix)
352
return version >= constraint
354
def _match_eq(self, version, constraint, prefix):
355
version, constraint = self._adjust_local(version, constraint, prefix)
357
result = (version == constraint)
359
result = _match_prefix(version, constraint)
362
def _match_arbitrary(self, version, constraint, prefix):
363
return str(version) == str(constraint)
365
def _match_ne(self, version, constraint, prefix):
366
version, constraint = self._adjust_local(version, constraint, prefix)
368
result = (version != constraint)
370
result = not _match_prefix(version, constraint)
373
def _match_compatible(self, version, constraint, prefix):
374
version, constraint = self._adjust_local(version, constraint, prefix)
375
if version == constraint:
377
if version < constraint:
381
release_clause = constraint._release_clause
382
if len(release_clause) > 1:
383
release_clause = release_clause[:-1]
384
pfx = '.'.join([str(i) for i in release_clause])
385
return _match_prefix(version, pfx)
388
(re.compile('[.+-]$'), ''), # remove trailing puncts
389
(re.compile(r'^[.](\d)'), r'0.\1'), # .N -> 0.N at start
390
(re.compile('^[.-]'), ''), # remove leading puncts
391
(re.compile(r'^\((.*)\)$'), r'\1'), # remove parentheses
392
(re.compile(r'^v(ersion)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
393
(re.compile(r'^r(ev)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
394
(re.compile('[.]{2,}'), '.'), # multiple runs of '.'
395
(re.compile(r'\b(alfa|apha)\b'), 'alpha'), # misspelt alpha
396
(re.compile(r'\b(pre-alpha|prealpha)\b'),
397
'pre.alpha'), # standardise
398
(re.compile(r'\(beta\)$'), 'beta'), # remove parentheses
401
_SUFFIX_REPLACEMENTS = (
402
(re.compile('^[:~._+-]+'), ''), # remove leading puncts
403
(re.compile('[,*")([\]]'), ''), # remove unwanted chars
404
(re.compile('[~:+_ -]'), '.'), # replace illegal chars
405
(re.compile('[.]{2,}'), '.'), # multiple runs of '.'
406
(re.compile(r'\.$'), ''), # trailing '.'
409
_NUMERIC_PREFIX = re.compile(r'(\d+(\.\d+)*)')
412
def _suggest_semantic_version(s):
414
Try to suggest a semantic form for a version for which
415
_suggest_normalized_version couldn't come up with anything.
417
result = s.strip().lower()
418
for pat, repl in _REPLACEMENTS:
419
result = pat.sub(repl, result)
423
# Now look for numeric prefix, and separate it out from
425
#import pdb; pdb.set_trace()
426
m = _NUMERIC_PREFIX.match(result)
431
prefix = m.groups()[0].split('.')
432
prefix = [int(i) for i in prefix]
433
while len(prefix) < 3:
436
suffix = result[m.end():]
438
suffix = '.'.join([str(i) for i in prefix[3:]]) + result[m.end():]
440
prefix = '.'.join([str(i) for i in prefix])
441
suffix = suffix.strip()
443
#import pdb; pdb.set_trace()
444
# massage the suffix.
445
for pat, repl in _SUFFIX_REPLACEMENTS:
446
suffix = pat.sub(repl, suffix)
451
sep = '-' if 'dev' in suffix else '+'
452
result = prefix + sep + suffix
453
if not is_semver(result):
458
def _suggest_normalized_version(s):
459
"""Suggest a normalized version close to the given version string.
461
If you have a version string that isn't rational (i.e. NormalizedVersion
462
doesn't like it) then you might be able to get an equivalent (or close)
463
rational version from this function.
465
This does a number of simple normalizations to the given string, based
466
on observation of versions currently in use on PyPI. Given a dump of
467
those version during PyCon 2009, 4287 of them:
468
- 2312 (53.93%) match NormalizedVersion without change
469
with the automatic suggestion
470
- 3474 (81.04%) match when using this suggestion method
472
@param s {str} An irrational version string.
473
@returns A rational version string, or None, if couldn't determine one.
477
return s # already rational
478
except UnsupportedVersionError:
483
# part of this could use maketrans
484
for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
485
('beta', 'b'), ('rc', 'c'), ('-final', ''),
487
('-release', ''), ('.release', ''), ('-stable', ''),
488
('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
490
rs = rs.replace(orig, repl)
492
# if something ends with dev or pre, we add a 0
493
rs = re.sub(r"pre$", r"pre0", rs)
494
rs = re.sub(r"dev$", r"dev0", rs)
496
# if we have something like "b-2" or "a.2" at the end of the
497
# version, that is pobably beta, alpha, etc
498
# let's remove the dash or dot
499
rs = re.sub(r"([abc]|rc)[\-\.](\d+)$", r"\1\2", rs)
501
# 1.0-dev-r371 -> 1.0.dev371
502
# 0.1-dev-r79 -> 0.1.dev79
503
rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
505
# Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
506
rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
509
if rs.startswith('v'):
512
# Clean leading '0's on numbers.
513
#TODO: unintended side-effect on, e.g., "2003.05.09"
514
# PyPI stats: 77 (~2%) better
515
rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
517
# Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers
519
# PyPI stats: 245 (7.56%) better
520
rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
522
# the 'dev-rNNN' tag is a dev tag
523
rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
525
# clean the - when used as a pre delimiter
526
rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
528
# a terminal "dev" or "devel" can be changed into ".dev0"
529
rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
531
# a terminal "dev" can be changed into ".dev0"
532
rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
534
# a terminal "final" or "stable" can be removed
535
rs = re.sub(r"(final|stable)$", "", rs)
537
# The 'r' and the '-' tags are post release tags
538
# 0.4a1.r10 -> 0.4a1.post10
539
# 0.9.33-17222 -> 0.9.33.post17222
540
# 0.9.33-r17222 -> 0.9.33.post17222
541
rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
543
# Clean 'r' instead of 'dev' usage:
544
# 0.9.33+r17222 -> 0.9.33.dev17222
545
# 1.0dev123 -> 1.0.dev123
546
# 1.0.git123 -> 1.0.dev123
547
# 1.0.bzr123 -> 1.0.dev123
548
# 0.1a0dev.123 -> 0.1a0.dev123
549
# PyPI stats: ~150 (~4%) better
550
rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
552
# Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
555
# 1.0preview123 -> 1.0c123
556
# PyPI stats: ~21 (0.62%) better
557
rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
559
# Tcl/Tk uses "px" for their post release markers
560
rs = re.sub(r"p(\d+)$", r".post\1", rs)
564
except UnsupportedVersionError:
569
# Legacy version processing (distribute-compatible)
572
_VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I)
587
for p in _VERSION_PART.split(s.lower()):
588
p = _VERSION_REPLACE.get(p, p)
590
if '0' <= p[:1] <= '9':
595
result.append('*final')
599
for p in get_parts(s):
600
if p.startswith('*'):
602
while result and result[-1] == '*final-':
604
while result and result[-1] == '00000000':
610
class LegacyVersion(Version):
612
return _legacy_key(s)
615
def is_prerelease(self):
617
for x in self._parts:
618
if (isinstance(x, string_types) and x.startswith('*') and
625
class LegacyMatcher(Matcher):
626
version_class = LegacyVersion
628
_operators = dict(Matcher._operators)
629
_operators['~='] = '_match_compatible'
631
numeric_re = re.compile('^(\d+(\.\d+)*)')
633
def _match_compatible(self, version, constraint, prefix):
634
if version < constraint:
636
m = self.numeric_re.match(str(constraint))
638
logger.warning('Cannot compute compatible match for version %s '
639
' and constraint %s', version, constraint)
643
s = s.rsplit('.', 1)[0]
644
return _match_prefix(version, s)
647
# Semantic versioning
650
_SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)'
651
r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?'
652
r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I)
656
return _SEMVER_RE.match(s)
659
def _semantic_key(s):
660
def make_tuple(s, absent):
664
parts = s[1:].split('.')
665
# We can't compare ints and strings on Python 3, so fudge it
666
# by zero-filling numeric values so simulate a numeric comparison
667
result = tuple([p.zfill(8) if p.isdigit() else p for p in parts])
672
raise UnsupportedVersionError(s)
674
major, minor, patch = [int(i) for i in groups[:3]]
675
# choose the '|' and '*' so that versions sort correctly
676
pre, build = make_tuple(groups[3], '|'), make_tuple(groups[5], '*')
677
return (major, minor, patch), pre, build
680
class SemanticVersion(Version):
682
return _semantic_key(s)
685
def is_prerelease(self):
686
return self._parts[1][0] != '|'
689
class SemanticMatcher(Matcher):
690
version_class = SemanticVersion
693
class VersionScheme(object):
694
def __init__(self, key, matcher, suggester=None):
696
self.matcher = matcher
697
self.suggester = suggester
699
def is_valid_version(self, s):
701
self.matcher.version_class(s)
703
except UnsupportedVersionError:
707
def is_valid_matcher(self, s):
711
except UnsupportedVersionError:
715
def is_valid_constraint_list(self, s):
717
Used for processing some metadata fields
719
return self.is_valid_matcher('dummy_name (%s)' % s)
721
def suggest(self, s):
722
if self.suggester is None:
725
result = self.suggester(s)
729
'normalized': VersionScheme(_normalized_key, NormalizedMatcher,
730
_suggest_normalized_version),
731
'legacy': VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s),
732
'semantic': VersionScheme(_semantic_key, SemanticMatcher,
733
_suggest_semantic_version),
736
_SCHEMES['default'] = _SCHEMES['normalized']
739
def get_scheme(name):
740
if name not in _SCHEMES:
741
raise ValueError('unknown scheme name: %r' % name)
742
return _SCHEMES[name]