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