Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/distlib/version.py: 45%
397 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-17 07:30 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-17 07:30 +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|b|c|rc)(\d+))?'
180 r'(\.(post)(\d+))?(\.(dev)(\d+))?'
181 r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$')
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 pre = pre[0], int(pre[1])
206 if post == (None, None):
207 post = ()
208 else:
209 post = post[0], int(post[1])
210 if dev == (None, None):
211 dev = ()
212 else:
213 dev = dev[0], int(dev[1])
214 if local is None:
215 local = ()
216 else:
217 parts = []
218 for part in local.split('.'):
219 # to ensure that numeric compares as > lexicographic, avoid
220 # comparing them directly, but encode a tuple which ensures
221 # correct sorting
222 if part.isdigit():
223 part = (1, int(part))
224 else:
225 part = (0, part)
226 parts.append(part)
227 local = tuple(parts)
228 if not pre:
229 # either before pre-release, or final release and after
230 if not post and dev:
231 # before pre-release
232 pre = ('a', -1) # to sort before a0
233 else:
234 pre = ('z',) # to sort after all pre-releases
235 # now look at the state of post and dev.
236 if not post:
237 post = ('_',) # sort before 'a'
238 if not dev:
239 dev = ('final',)
241 #print('%s -> %s' % (s, m.groups()))
242 return epoch, nums, pre, post, dev, local
245_normalized_key = _pep_440_key
248class NormalizedVersion(Version):
249 """A rational version.
251 Good:
252 1.2 # equivalent to "1.2.0"
253 1.2.0
254 1.2a1
255 1.2.3a2
256 1.2.3b1
257 1.2.3c1
258 1.2.3.4
259 TODO: fill this out
261 Bad:
262 1 # minimum two numbers
263 1.2a # release level must have a release serial
264 1.2.3b
265 """
266 def parse(self, s):
267 result = _normalized_key(s)
268 # _normalized_key loses trailing zeroes in the release
269 # clause, since that's needed to ensure that X.Y == X.Y.0 == X.Y.0.0
270 # However, PEP 440 prefix matching needs it: for example,
271 # (~= 1.4.5.0) matches differently to (~= 1.4.5.0.0).
272 m = PEP440_VERSION_RE.match(s) # must succeed
273 groups = m.groups()
274 self._release_clause = tuple(int(v) for v in groups[1].split('.'))
275 return result
277 PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev'])
279 @property
280 def is_prerelease(self):
281 return any(t[0] in self.PREREL_TAGS for t in self._parts if t)
284def _match_prefix(x, y):
285 x = str(x)
286 y = str(y)
287 if x == y:
288 return True
289 if not x.startswith(y):
290 return False
291 n = len(y)
292 return x[n] == '.'
295class NormalizedMatcher(Matcher):
296 version_class = NormalizedVersion
298 # value is either a callable or the name of a method
299 _operators = {
300 '~=': '_match_compatible',
301 '<': '_match_lt',
302 '>': '_match_gt',
303 '<=': '_match_le',
304 '>=': '_match_ge',
305 '==': '_match_eq',
306 '===': '_match_arbitrary',
307 '!=': '_match_ne',
308 }
310 def _adjust_local(self, version, constraint, prefix):
311 if prefix:
312 strip_local = '+' not in constraint and version._parts[-1]
313 else:
314 # both constraint and version are
315 # NormalizedVersion instances.
316 # If constraint does not have a local component,
317 # ensure the version doesn't, either.
318 strip_local = not constraint._parts[-1] and version._parts[-1]
319 if strip_local:
320 s = version._string.split('+', 1)[0]
321 version = self.version_class(s)
322 return version, constraint
324 def _match_lt(self, version, constraint, prefix):
325 version, constraint = self._adjust_local(version, constraint, prefix)
326 if version >= constraint:
327 return False
328 release_clause = constraint._release_clause
329 pfx = '.'.join([str(i) for i in release_clause])
330 return not _match_prefix(version, pfx)
332 def _match_gt(self, version, constraint, prefix):
333 version, constraint = self._adjust_local(version, constraint, prefix)
334 if version <= constraint:
335 return False
336 release_clause = constraint._release_clause
337 pfx = '.'.join([str(i) for i in release_clause])
338 return not _match_prefix(version, pfx)
340 def _match_le(self, version, constraint, prefix):
341 version, constraint = self._adjust_local(version, constraint, prefix)
342 return version <= constraint
344 def _match_ge(self, version, constraint, prefix):
345 version, constraint = self._adjust_local(version, constraint, prefix)
346 return version >= constraint
348 def _match_eq(self, version, constraint, prefix):
349 version, constraint = self._adjust_local(version, constraint, prefix)
350 if not prefix:
351 result = (version == constraint)
352 else:
353 result = _match_prefix(version, constraint)
354 return result
356 def _match_arbitrary(self, version, constraint, prefix):
357 return str(version) == str(constraint)
359 def _match_ne(self, version, constraint, prefix):
360 version, constraint = self._adjust_local(version, constraint, prefix)
361 if not prefix:
362 result = (version != constraint)
363 else:
364 result = not _match_prefix(version, constraint)
365 return result
367 def _match_compatible(self, version, constraint, prefix):
368 version, constraint = self._adjust_local(version, constraint, prefix)
369 if version == constraint:
370 return True
371 if version < constraint:
372 return False
373# if not prefix:
374# return True
375 release_clause = constraint._release_clause
376 if len(release_clause) > 1:
377 release_clause = release_clause[:-1]
378 pfx = '.'.join([str(i) for i in release_clause])
379 return _match_prefix(version, pfx)
381_REPLACEMENTS = (
382 (re.compile('[.+-]$'), ''), # remove trailing puncts
383 (re.compile(r'^[.](\d)'), r'0.\1'), # .N -> 0.N at start
384 (re.compile('^[.-]'), ''), # remove leading puncts
385 (re.compile(r'^\((.*)\)$'), r'\1'), # remove parentheses
386 (re.compile(r'^v(ersion)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
387 (re.compile(r'^r(ev)?\s*(\d+)'), r'\2'), # remove leading v(ersion)
388 (re.compile('[.]{2,}'), '.'), # multiple runs of '.'
389 (re.compile(r'\b(alfa|apha)\b'), 'alpha'), # misspelt alpha
390 (re.compile(r'\b(pre-alpha|prealpha)\b'),
391 'pre.alpha'), # standardise
392 (re.compile(r'\(beta\)$'), 'beta'), # remove parentheses
393)
395_SUFFIX_REPLACEMENTS = (
396 (re.compile('^[:~._+-]+'), ''), # remove leading puncts
397 (re.compile('[,*")([\\]]'), ''), # remove unwanted chars
398 (re.compile('[~:+_ -]'), '.'), # replace illegal chars
399 (re.compile('[.]{2,}'), '.'), # multiple runs of '.'
400 (re.compile(r'\.$'), ''), # trailing '.'
401)
403_NUMERIC_PREFIX = re.compile(r'(\d+(\.\d+)*)')
406def _suggest_semantic_version(s):
407 """
408 Try to suggest a semantic form for a version for which
409 _suggest_normalized_version couldn't come up with anything.
410 """
411 result = s.strip().lower()
412 for pat, repl in _REPLACEMENTS:
413 result = pat.sub(repl, result)
414 if not result:
415 result = '0.0.0'
417 # Now look for numeric prefix, and separate it out from
418 # the rest.
419 #import pdb; pdb.set_trace()
420 m = _NUMERIC_PREFIX.match(result)
421 if not m:
422 prefix = '0.0.0'
423 suffix = result
424 else:
425 prefix = m.groups()[0].split('.')
426 prefix = [int(i) for i in prefix]
427 while len(prefix) < 3:
428 prefix.append(0)
429 if len(prefix) == 3:
430 suffix = result[m.end():]
431 else:
432 suffix = '.'.join([str(i) for i in prefix[3:]]) + result[m.end():]
433 prefix = prefix[:3]
434 prefix = '.'.join([str(i) for i in prefix])
435 suffix = suffix.strip()
436 if suffix:
437 #import pdb; pdb.set_trace()
438 # massage the suffix.
439 for pat, repl in _SUFFIX_REPLACEMENTS:
440 suffix = pat.sub(repl, suffix)
442 if not suffix:
443 result = prefix
444 else:
445 sep = '-' if 'dev' in suffix else '+'
446 result = prefix + sep + suffix
447 if not is_semver(result):
448 result = None
449 return result
452def _suggest_normalized_version(s):
453 """Suggest a normalized version close to the given version string.
455 If you have a version string that isn't rational (i.e. NormalizedVersion
456 doesn't like it) then you might be able to get an equivalent (or close)
457 rational version from this function.
459 This does a number of simple normalizations to the given string, based
460 on observation of versions currently in use on PyPI. Given a dump of
461 those version during PyCon 2009, 4287 of them:
462 - 2312 (53.93%) match NormalizedVersion without change
463 with the automatic suggestion
464 - 3474 (81.04%) match when using this suggestion method
466 @param s {str} An irrational version string.
467 @returns A rational version string, or None, if couldn't determine one.
468 """
469 try:
470 _normalized_key(s)
471 return s # already rational
472 except UnsupportedVersionError:
473 pass
475 rs = s.lower()
477 # part of this could use maketrans
478 for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
479 ('beta', 'b'), ('rc', 'c'), ('-final', ''),
480 ('-pre', 'c'),
481 ('-release', ''), ('.release', ''), ('-stable', ''),
482 ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
483 ('final', '')):
484 rs = rs.replace(orig, repl)
486 # if something ends with dev or pre, we add a 0
487 rs = re.sub(r"pre$", r"pre0", rs)
488 rs = re.sub(r"dev$", r"dev0", rs)
490 # if we have something like "b-2" or "a.2" at the end of the
491 # version, that is probably beta, alpha, etc
492 # let's remove the dash or dot
493 rs = re.sub(r"([abc]|rc)[\-\.](\d+)$", r"\1\2", rs)
495 # 1.0-dev-r371 -> 1.0.dev371
496 # 0.1-dev-r79 -> 0.1.dev79
497 rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
499 # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
500 rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
502 # Clean: v0.3, v1.0
503 if rs.startswith('v'):
504 rs = rs[1:]
506 # Clean leading '0's on numbers.
507 #TODO: unintended side-effect on, e.g., "2003.05.09"
508 # PyPI stats: 77 (~2%) better
509 rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
511 # Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers
512 # zero.
513 # PyPI stats: 245 (7.56%) better
514 rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
516 # the 'dev-rNNN' tag is a dev tag
517 rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
519 # clean the - when used as a pre delimiter
520 rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
522 # a terminal "dev" or "devel" can be changed into ".dev0"
523 rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
525 # a terminal "dev" can be changed into ".dev0"
526 rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
528 # a terminal "final" or "stable" can be removed
529 rs = re.sub(r"(final|stable)$", "", rs)
531 # The 'r' and the '-' tags are post release tags
532 # 0.4a1.r10 -> 0.4a1.post10
533 # 0.9.33-17222 -> 0.9.33.post17222
534 # 0.9.33-r17222 -> 0.9.33.post17222
535 rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
537 # Clean 'r' instead of 'dev' usage:
538 # 0.9.33+r17222 -> 0.9.33.dev17222
539 # 1.0dev123 -> 1.0.dev123
540 # 1.0.git123 -> 1.0.dev123
541 # 1.0.bzr123 -> 1.0.dev123
542 # 0.1a0dev.123 -> 0.1a0.dev123
543 # PyPI stats: ~150 (~4%) better
544 rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
546 # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
547 # 0.2.pre1 -> 0.2c1
548 # 0.2-c1 -> 0.2c1
549 # 1.0preview123 -> 1.0c123
550 # PyPI stats: ~21 (0.62%) better
551 rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
553 # Tcl/Tk uses "px" for their post release markers
554 rs = re.sub(r"p(\d+)$", r".post\1", rs)
556 try:
557 _normalized_key(rs)
558 except UnsupportedVersionError:
559 rs = None
560 return rs
562#
563# Legacy version processing (distribute-compatible)
564#
566_VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I)
567_VERSION_REPLACE = {
568 'pre': 'c',
569 'preview': 'c',
570 '-': 'final-',
571 'rc': 'c',
572 'dev': '@',
573 '': None,
574 '.': None,
575}
578def _legacy_key(s):
579 def get_parts(s):
580 result = []
581 for p in _VERSION_PART.split(s.lower()):
582 p = _VERSION_REPLACE.get(p, p)
583 if p:
584 if '0' <= p[:1] <= '9':
585 p = p.zfill(8)
586 else:
587 p = '*' + p
588 result.append(p)
589 result.append('*final')
590 return result
592 result = []
593 for p in get_parts(s):
594 if p.startswith('*'):
595 if p < '*final':
596 while result and result[-1] == '*final-':
597 result.pop()
598 while result and result[-1] == '00000000':
599 result.pop()
600 result.append(p)
601 return tuple(result)
604class LegacyVersion(Version):
605 def parse(self, s):
606 return _legacy_key(s)
608 @property
609 def is_prerelease(self):
610 result = False
611 for x in self._parts:
612 if (isinstance(x, string_types) and x.startswith('*') and
613 x < '*final'):
614 result = True
615 break
616 return result
619class LegacyMatcher(Matcher):
620 version_class = LegacyVersion
622 _operators = dict(Matcher._operators)
623 _operators['~='] = '_match_compatible'
625 numeric_re = re.compile(r'^(\d+(\.\d+)*)')
627 def _match_compatible(self, version, constraint, prefix):
628 if version < constraint:
629 return False
630 m = self.numeric_re.match(str(constraint))
631 if not m:
632 logger.warning('Cannot compute compatible match for version %s '
633 ' and constraint %s', version, constraint)
634 return True
635 s = m.groups()[0]
636 if '.' in s:
637 s = s.rsplit('.', 1)[0]
638 return _match_prefix(version, s)
640#
641# Semantic versioning
642#
644_SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)'
645 r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?'
646 r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I)
649def is_semver(s):
650 return _SEMVER_RE.match(s)
653def _semantic_key(s):
654 def make_tuple(s, absent):
655 if s is None:
656 result = (absent,)
657 else:
658 parts = s[1:].split('.')
659 # We can't compare ints and strings on Python 3, so fudge it
660 # by zero-filling numeric values so simulate a numeric comparison
661 result = tuple([p.zfill(8) if p.isdigit() else p for p in parts])
662 return result
664 m = is_semver(s)
665 if not m:
666 raise UnsupportedVersionError(s)
667 groups = m.groups()
668 major, minor, patch = [int(i) for i in groups[:3]]
669 # choose the '|' and '*' so that versions sort correctly
670 pre, build = make_tuple(groups[3], '|'), make_tuple(groups[5], '*')
671 return (major, minor, patch), pre, build
674class SemanticVersion(Version):
675 def parse(self, s):
676 return _semantic_key(s)
678 @property
679 def is_prerelease(self):
680 return self._parts[1][0] != '|'
683class SemanticMatcher(Matcher):
684 version_class = SemanticVersion
687class VersionScheme(object):
688 def __init__(self, key, matcher, suggester=None):
689 self.key = key
690 self.matcher = matcher
691 self.suggester = suggester
693 def is_valid_version(self, s):
694 try:
695 self.matcher.version_class(s)
696 result = True
697 except UnsupportedVersionError:
698 result = False
699 return result
701 def is_valid_matcher(self, s):
702 try:
703 self.matcher(s)
704 result = True
705 except UnsupportedVersionError:
706 result = False
707 return result
709 def is_valid_constraint_list(self, s):
710 """
711 Used for processing some metadata fields
712 """
713 # See issue #140. Be tolerant of a single trailing comma.
714 if s.endswith(','):
715 s = s[:-1]
716 return self.is_valid_matcher('dummy_name (%s)' % s)
718 def suggest(self, s):
719 if self.suggester is None:
720 result = None
721 else:
722 result = self.suggester(s)
723 return result
725_SCHEMES = {
726 'normalized': VersionScheme(_normalized_key, NormalizedMatcher,
727 _suggest_normalized_version),
728 'legacy': VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s),
729 'semantic': VersionScheme(_semantic_key, SemanticMatcher,
730 _suggest_semantic_version),
731}
733_SCHEMES['default'] = _SCHEMES['normalized']
736def get_scheme(name):
737 if name not in _SCHEMES:
738 raise ValueError('unknown scheme name: %r' % name)
739 return _SCHEMES[name]