Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/distlib/version.py: 24%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2012-2023 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 return epoch, nums, pre, post, dev, local
253_normalized_key = _pep_440_key
256class NormalizedVersion(Version):
257 """A rational version.
259 Good:
260 1.2 # equivalent to "1.2.0"
261 1.2.0
262 1.2a1
263 1.2.3a2
264 1.2.3b1
265 1.2.3c1
266 1.2.3.4
267 TODO: fill this out
269 Bad:
270 1 # minimum two numbers
271 1.2a # release level must have a release serial
272 1.2.3b
273 """
274 def parse(self, s):
275 result = _normalized_key(s)
276 # _normalized_key loses trailing zeroes in the release
277 # clause, since that's needed to ensure that X.Y == X.Y.0 == X.Y.0.0
278 # However, PEP 440 prefix matching needs it: for example,
279 # (~= 1.4.5.0) matches differently to (~= 1.4.5.0.0).
280 m = PEP440_VERSION_RE.match(s) # must succeed
281 groups = m.groups()
282 self._release_clause = tuple(int(v) for v in groups[1].split('.'))
283 return result
285 PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev'])
287 @property
288 def is_prerelease(self):
289 return any(t[0] in self.PREREL_TAGS for t in self._parts if t)
292def _match_prefix(x, y):
293 x = str(x)
294 y = str(y)
295 if x == y:
296 return True
297 if not x.startswith(y):
298 return False
299 n = len(y)
300 return x[n] == '.'
303class NormalizedMatcher(Matcher):
304 version_class = NormalizedVersion
306 # value is either a callable or the name of a method
307 _operators = {
308 '~=': '_match_compatible',
309 '<': '_match_lt',
310 '>': '_match_gt',
311 '<=': '_match_le',
312 '>=': '_match_ge',
313 '==': '_match_eq',
314 '===': '_match_arbitrary',
315 '!=': '_match_ne',
316 }
318 def _adjust_local(self, version, constraint, prefix):
319 if prefix:
320 strip_local = '+' not in constraint and version._parts[-1]
321 else:
322 # both constraint and version are
323 # NormalizedVersion instances.
324 # If constraint does not have a local component,
325 # ensure the version doesn't, either.
326 strip_local = not constraint._parts[-1] and version._parts[-1]
327 if strip_local:
328 s = version._string.split('+', 1)[0]
329 version = self.version_class(s)
330 return version, constraint
332 def _match_lt(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_gt(self, version, constraint, prefix):
341 version, constraint = self._adjust_local(version, constraint, prefix)
342 if version <= constraint:
343 return False
344 release_clause = constraint._release_clause
345 pfx = '.'.join([str(i) for i in release_clause])
346 return not _match_prefix(version, pfx)
348 def _match_le(self, version, constraint, prefix):
349 version, constraint = self._adjust_local(version, constraint, prefix)
350 return version <= constraint
352 def _match_ge(self, version, constraint, prefix):
353 version, constraint = self._adjust_local(version, constraint, prefix)
354 return version >= constraint
356 def _match_eq(self, version, constraint, prefix):
357 version, constraint = self._adjust_local(version, constraint, prefix)
358 if not prefix:
359 result = (version == constraint)
360 else:
361 result = _match_prefix(version, constraint)
362 return result
364 def _match_arbitrary(self, version, constraint, prefix):
365 return str(version) == str(constraint)
367 def _match_ne(self, version, constraint, prefix):
368 version, constraint = self._adjust_local(version, constraint, prefix)
369 if not prefix:
370 result = (version != constraint)
371 else:
372 result = not _match_prefix(version, constraint)
373 return result
375 def _match_compatible(self, version, constraint, prefix):
376 version, constraint = self._adjust_local(version, constraint, prefix)
377 if version == constraint:
378 return True
379 if version < constraint:
380 return False
381# if not prefix:
382# return True
383 release_clause = constraint._release_clause
384 if len(release_clause) > 1:
385 release_clause = release_clause[:-1]
386 pfx = '.'.join([str(i) for i in release_clause])
387 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#
576_VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I)
577_VERSION_REPLACE = {
578 'pre': 'c',
579 'preview': 'c',
580 '-': 'final-',
581 'rc': 'c',
582 'dev': '@',
583 '': None,
584 '.': None,
585}
588def _legacy_key(s):
589 def get_parts(s):
590 result = []
591 for p in _VERSION_PART.split(s.lower()):
592 p = _VERSION_REPLACE.get(p, p)
593 if p:
594 if '0' <= p[:1] <= '9':
595 p = p.zfill(8)
596 else:
597 p = '*' + p
598 result.append(p)
599 result.append('*final')
600 return result
602 result = []
603 for p in get_parts(s):
604 if p.startswith('*'):
605 if p < '*final':
606 while result and result[-1] == '*final-':
607 result.pop()
608 while result and result[-1] == '00000000':
609 result.pop()
610 result.append(p)
611 return tuple(result)
614class LegacyVersion(Version):
615 def parse(self, s):
616 return _legacy_key(s)
618 @property
619 def is_prerelease(self):
620 result = False
621 for x in self._parts:
622 if (isinstance(x, string_types) and x.startswith('*') and 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#
654_SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)'
655 r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?'
656 r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I)
659def is_semver(s):
660 return _SEMVER_RE.match(s)
663def _semantic_key(s):
664 def make_tuple(s, absent):
665 if s is None:
666 result = (absent,)
667 else:
668 parts = s[1:].split('.')
669 # We can't compare ints and strings on Python 3, so fudge it
670 # by zero-filling numeric values so simulate a numeric comparison
671 result = tuple([p.zfill(8) if p.isdigit() else p for p in parts])
672 return result
674 m = is_semver(s)
675 if not m:
676 raise UnsupportedVersionError(s)
677 groups = m.groups()
678 major, minor, patch = [int(i) for i in groups[:3]]
679 # choose the '|' and '*' so that versions sort correctly
680 pre, build = make_tuple(groups[3], '|'), make_tuple(groups[5], '*')
681 return (major, minor, patch), pre, build
684class SemanticVersion(Version):
685 def parse(self, s):
686 return _semantic_key(s)
688 @property
689 def is_prerelease(self):
690 return self._parts[1][0] != '|'
693class SemanticMatcher(Matcher):
694 version_class = SemanticVersion
697class VersionScheme(object):
698 def __init__(self, key, matcher, suggester=None):
699 self.key = key
700 self.matcher = matcher
701 self.suggester = suggester
703 def is_valid_version(self, s):
704 try:
705 self.matcher.version_class(s)
706 result = True
707 except UnsupportedVersionError:
708 result = False
709 return result
711 def is_valid_matcher(self, s):
712 try:
713 self.matcher(s)
714 result = True
715 except UnsupportedVersionError:
716 result = False
717 return result
719 def is_valid_constraint_list(self, s):
720 """
721 Used for processing some metadata fields
722 """
723 # See issue #140. Be tolerant of a single trailing comma.
724 if s.endswith(','):
725 s = s[:-1]
726 return self.is_valid_matcher('dummy_name (%s)' % s)
728 def suggest(self, s):
729 if self.suggester is None:
730 result = None
731 else:
732 result = self.suggester(s)
733 return result
736_SCHEMES = {
737 'normalized': VersionScheme(_normalized_key, NormalizedMatcher,
738 _suggest_normalized_version),
739 'legacy': VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s),
740 'semantic': VersionScheme(_semantic_key, SemanticMatcher,
741 _suggest_semantic_version),
742}
744_SCHEMES['default'] = _SCHEMES['normalized']
747def get_scheme(name):
748 if name not in _SCHEMES:
749 raise ValueError('unknown scheme name: %r' % name)
750 return _SCHEMES[name]