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

404 statements  

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""" 

10 

11import logging 

12import re 

13 

14from .compat import string_types 

15from .util import parse_requirement 

16 

17__all__ = ['NormalizedVersion', 'NormalizedMatcher', 

18 'LegacyVersion', 'LegacyMatcher', 

19 'SemanticVersion', 'SemanticMatcher', 

20 'UnsupportedVersionError', 'get_scheme'] 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25class UnsupportedVersionError(ValueError): 

26 """This is an unsupported version.""" 

27 pass 

28 

29 

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 

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 

79class Matcher(object): 

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 

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) 

182 

183 

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] 

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 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',) 

249 

250 return epoch, nums, pre, post, dev, local 

251 

252 

253_normalized_key = _pep_440_key 

254 

255 

256class NormalizedVersion(Version): 

257 """A rational version. 

258 

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 

268 

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 

284 

285 PREREL_TAGS = set(['a', 'b', 'c', 'rc', 'dev']) 

286 

287 @property 

288 def is_prerelease(self): 

289 return any(t[0] in self.PREREL_TAGS for t in self._parts if t) 

290 

291 

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] == '.' 

301 

302 

303class NormalizedMatcher(Matcher): 

304 version_class = NormalizedVersion 

305 

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 } 

317 

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 

331 

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) 

339 

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) 

347 

348 def _match_le(self, version, constraint, prefix): 

349 version, constraint = self._adjust_local(version, constraint, prefix) 

350 return version <= constraint 

351 

352 def _match_ge(self, version, constraint, prefix): 

353 version, constraint = self._adjust_local(version, constraint, prefix) 

354 return version >= constraint 

355 

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 

363 

364 def _match_arbitrary(self, version, constraint, prefix): 

365 return str(version) == str(constraint) 

366 

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 

374 

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) 

388 

389 

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) 

403 

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) 

411 

412_NUMERIC_PREFIX = re.compile(r'(\d+(\.\d+)*)') 

413 

414 

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' 

425 

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) 

450 

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 

459 

460 

461def _suggest_normalized_version(s): 

462 """Suggest a normalized version close to the given version string. 

463 

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. 

467 

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 

474 

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 

483 

484 rs = s.lower() 

485 

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) 

494 

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) 

498 

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) 

503 

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) 

507 

508 # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1 

509 rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs) 

510 

511 # Clean: v0.3, v1.0 

512 if rs.startswith('v'): 

513 rs = rs[1:] 

514 

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) 

519 

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) 

524 

525 # the 'dev-rNNN' tag is a dev tag 

526 rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs) 

527 

528 # clean the - when used as a pre delimiter 

529 rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs) 

530 

531 # a terminal "dev" or "devel" can be changed into ".dev0" 

532 rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs) 

533 

534 # a terminal "dev" can be changed into ".dev0" 

535 rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs) 

536 

537 # a terminal "final" or "stable" can be removed 

538 rs = re.sub(r"(final|stable)$", "", rs) 

539 

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) 

545 

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) 

554 

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) 

561 

562 # Tcl/Tk uses "px" for their post release markers 

563 rs = re.sub(r"p(\d+)$", r".post\1", rs) 

564 

565 try: 

566 _normalized_key(rs) 

567 except UnsupportedVersionError: 

568 rs = None 

569 return rs 

570 

571# 

572# Legacy version processing (distribute-compatible) 

573# 

574 

575 

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} 

586 

587 

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 

601 

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) 

612 

613 

614class LegacyVersion(Version): 

615 def parse(self, s): 

616 return _legacy_key(s) 

617 

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 

626 

627 

628class LegacyMatcher(Matcher): 

629 version_class = LegacyVersion 

630 

631 _operators = dict(Matcher._operators) 

632 _operators['~='] = '_match_compatible' 

633 

634 numeric_re = re.compile(r'^(\d+(\.\d+)*)') 

635 

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) 

648 

649# 

650# Semantic versioning 

651# 

652 

653 

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) 

657 

658 

659def is_semver(s): 

660 return _SEMVER_RE.match(s) 

661 

662 

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 

673 

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 

682 

683 

684class SemanticVersion(Version): 

685 def parse(self, s): 

686 return _semantic_key(s) 

687 

688 @property 

689 def is_prerelease(self): 

690 return self._parts[1][0] != '|' 

691 

692 

693class SemanticMatcher(Matcher): 

694 version_class = SemanticVersion 

695 

696 

697class VersionScheme(object): 

698 def __init__(self, key, matcher, suggester=None): 

699 self.key = key 

700 self.matcher = matcher 

701 self.suggester = suggester 

702 

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 

710 

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 

718 

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) 

727 

728 def suggest(self, s): 

729 if self.suggester is None: 

730 result = None 

731 else: 

732 result = self.suggester(s) 

733 return result 

734 

735 

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} 

743 

744_SCHEMES['default'] = _SCHEMES['normalized'] 

745 

746 

747def get_scheme(name): 

748 if name not in _SCHEMES: 

749 raise ValueError('unknown scheme name: %r' % name) 

750 return _SCHEMES[name]