python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
pip/
_vendor/
distlib/
version.py
       1  # -*- coding: utf-8 -*-
       2  #
       3  # Copyright (C) 2012-2017 The Python Software Foundation.
       4  # See LICENSE.txt and CONTRIBUTORS.txt.
       5  #
       6  """
       7  Implementation of a flexible versioning scheme providing support for PEP-440,
       8  setuptools-compatible and semantic versioning.
       9  """
      10  
      11  import logging
      12  import re
      13  
      14  from .compat import string_types
      15  from .util import parse_requirement
      16  
      17  __all__ = ['NormalizedVersion', 'NormalizedMatcher',
      18             'LegacyVersion', 'LegacyMatcher',
      19             'SemanticVersion', 'SemanticMatcher',
      20             'UnsupportedVersionError', 'get_scheme']
      21  
      22  logger = logging.getLogger(__name__)
      23  
      24  
      25  class ESC[4;38;5;81mUnsupportedVersionError(ESC[4;38;5;149mValueError):
      26      """This is an unsupported version."""
      27      pass
      28  
      29  
      30  class ESC[4;38;5;81mVersion(ESC[4;38;5;149mobject):
      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
      36  
      37      def parse(self, s):
      38          raise NotImplementedError('please implement in a subclass')
      39  
      40      def _check_compatible(self, other):
      41          if type(self) != type(other):
      42              raise TypeError('cannot compare %r and %r' % (self, other))
      43  
      44      def __eq__(self, other):
      45          self._check_compatible(other)
      46          return self._parts == other._parts
      47  
      48      def __ne__(self, other):
      49          return not self.__eq__(other)
      50  
      51      def __lt__(self, other):
      52          self._check_compatible(other)
      53          return self._parts < other._parts
      54  
      55      def __gt__(self, other):
      56          return not (self.__lt__(other) or self.__eq__(other))
      57  
      58      def __le__(self, other):
      59          return self.__lt__(other) or self.__eq__(other)
      60  
      61      def __ge__(self, other):
      62          return self.__gt__(other) or self.__eq__(other)
      63  
      64      # See http://docs.python.org/reference/datamodel#object.__hash__
      65      def __hash__(self):
      66          return hash(self._parts)
      67  
      68      def __repr__(self):
      69          return "%s('%s')" % (self.__class__.__name__, self._string)
      70  
      71      def __str__(self):
      72          return self._string
      73  
      74      @property
      75      def is_prerelease(self):
      76          raise NotImplementedError('Please implement in subclasses.')
      77  
      78  
      79  class ESC[4;38;5;81mMatcher(ESC[4;38;5;149mobject):
      80      version_class = None
      81  
      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      }
      94  
      95      # this is a method only to support alternative implementations
      96      # via overriding
      97      def parse_requirement(self, s):
      98          return parse_requirement(s)
      99  
     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)
     128  
     129      def match(self, version):
     130          """
     131          Check if the provided version matches the constraints.
     132  
     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
     149  
     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
     156  
     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))
     160  
     161      def __eq__(self, other):
     162          self._check_compatible(other)
     163          return self.key == other.key and self._parts == other._parts
     164  
     165      def __ne__(self, other):
     166          return not self.__eq__(other)
     167  
     168      # See http://docs.python.org/reference/datamodel#object.__hash__
     169      def __hash__(self):
     170          return hash(self.key) + hash(self._parts)
     171  
     172      def __repr__(self):
     173          return "%s(%r)" % (self.__class__.__name__, self._string)
     174  
     175      def __str__(self):
     176          return self._string
     177  
     178  
     179  PEP440_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]+)?))?$')
     182  
     183  
     184  def _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]
     193  
     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',)
     240  
     241      #print('%s -> %s' % (s, m.groups()))
     242      return epoch, nums, pre, post, dev, local
     243  
     244  
     245  _normalized_key = _pep_440_key
     246  
     247  
     248  class ESC[4;38;5;81mNormalizedVersion(ESC[4;38;5;149mVersion):
     249      """A rational version.
     250  
     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
     260  
     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
     276  
     277      PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev'])
     278  
     279      @property
     280      def is_prerelease(self):
     281          return any(t[0] in self.PREREL_TAGS for t in self._parts if t)
     282  
     283  
     284  def _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] == '.'
     293  
     294  
     295  class ESC[4;38;5;81mNormalizedMatcher(ESC[4;38;5;149mMatcher):
     296      version_class = NormalizedVersion
     297  
     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      }
     309  
     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
     323  
     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)
     331  
     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)
     339  
     340      def _match_le(self, version, constraint, prefix):
     341          version, constraint = self._adjust_local(version, constraint, prefix)
     342          return version <= constraint
     343  
     344      def _match_ge(self, version, constraint, prefix):
     345          version, constraint = self._adjust_local(version, constraint, prefix)
     346          return version >= constraint
     347  
     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
     355  
     356      def _match_arbitrary(self, version, constraint, prefix):
     357          return str(version) == str(constraint)
     358  
     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
     366  
     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)
     380  
     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  )
     394  
     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  )
     402  
     403  _NUMERIC_PREFIX = re.compile(r'(\d+(\.\d+)*)')
     404  
     405  
     406  def _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'
     416  
     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)
     441  
     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
     450  
     451  
     452  def _suggest_normalized_version(s):
     453      """Suggest a normalized version close to the given version string.
     454  
     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.
     458  
     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
     465  
     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
     474  
     475      rs = s.lower()
     476  
     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)
     485  
     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)
     489  
     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)
     494  
     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)
     498  
     499      # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
     500      rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
     501  
     502      # Clean: v0.3, v1.0
     503      if rs.startswith('v'):
     504          rs = rs[1:]
     505  
     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)
     510  
     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)
     515  
     516      # the 'dev-rNNN' tag is a dev tag
     517      rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
     518  
     519      # clean the - when used as a pre delimiter
     520      rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
     521  
     522      # a terminal "dev" or "devel" can be changed into ".dev0"
     523      rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
     524  
     525      # a terminal "dev" can be changed into ".dev0"
     526      rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
     527  
     528      # a terminal "final" or "stable" can be removed
     529      rs = re.sub(r"(final|stable)$", "", rs)
     530  
     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)
     536  
     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)
     545  
     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)
     552  
     553      # Tcl/Tk uses "px" for their post release markers
     554      rs = re.sub(r"p(\d+)$", r".post\1", rs)
     555  
     556      try:
     557          _normalized_key(rs)
     558      except UnsupportedVersionError:
     559          rs = None
     560      return rs
     561  
     562  #
     563  #   Legacy version processing (distribute-compatible)
     564  #
     565  
     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  }
     576  
     577  
     578  def _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
     591  
     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)
     602  
     603  
     604  class ESC[4;38;5;81mLegacyVersion(ESC[4;38;5;149mVersion):
     605      def parse(self, s):
     606          return _legacy_key(s)
     607  
     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
     617  
     618  
     619  class ESC[4;38;5;81mLegacyMatcher(ESC[4;38;5;149mMatcher):
     620      version_class = LegacyVersion
     621  
     622      _operators = dict(Matcher._operators)
     623      _operators['~='] = '_match_compatible'
     624  
     625      numeric_re = re.compile(r'^(\d+(\.\d+)*)')
     626  
     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)
     639  
     640  #
     641  #   Semantic versioning
     642  #
     643  
     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)
     647  
     648  
     649  def is_semver(s):
     650      return _SEMVER_RE.match(s)
     651  
     652  
     653  def _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
     663  
     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
     672  
     673  
     674  class ESC[4;38;5;81mSemanticVersion(ESC[4;38;5;149mVersion):
     675      def parse(self, s):
     676          return _semantic_key(s)
     677  
     678      @property
     679      def is_prerelease(self):
     680          return self._parts[1][0] != '|'
     681  
     682  
     683  class ESC[4;38;5;81mSemanticMatcher(ESC[4;38;5;149mMatcher):
     684      version_class = SemanticVersion
     685  
     686  
     687  class ESC[4;38;5;81mVersionScheme(ESC[4;38;5;149mobject):
     688      def __init__(self, key, matcher, suggester=None):
     689          self.key = key
     690          self.matcher = matcher
     691          self.suggester = suggester
     692  
     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
     700  
     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
     708  
     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)
     717  
     718      def suggest(self, s):
     719          if self.suggester is None:
     720              result = None
     721          else:
     722              result = self.suggester(s)
     723          return result
     724  
     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  }
     732  
     733  _SCHEMES['default'] = _SCHEMES['normalized']
     734  
     735  
     736  def get_scheme(name):
     737      if name not in _SCHEMES:
     738          raise ValueError('unknown scheme name: %r' % name)
     739      return _SCHEMES[name]