Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/distlib/version.py: 46%

403 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:51 +0000

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

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 #print('%s -> %s' % (s, m.groups())) 

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

252 

253 

254_normalized_key = _pep_440_key 

255 

256 

257class NormalizedVersion(Version): 

258 """A rational version. 

259 

260 Good: 

261 1.2 # equivalent to "1.2.0" 

262 1.2.0 

263 1.2a1 

264 1.2.3a2 

265 1.2.3b1 

266 1.2.3c1 

267 1.2.3.4 

268 TODO: fill this out 

269 

270 Bad: 

271 1 # minimum two numbers 

272 1.2a # release level must have a release serial 

273 1.2.3b 

274 """ 

275 def parse(self, s): 

276 result = _normalized_key(s) 

277 # _normalized_key loses trailing zeroes in the release 

278 # clause, since that's needed to ensure that X.Y == X.Y.0 == X.Y.0.0 

279 # However, PEP 440 prefix matching needs it: for example, 

280 # (~= 1.4.5.0) matches differently to (~= 1.4.5.0.0). 

281 m = PEP440_VERSION_RE.match(s) # must succeed 

282 groups = m.groups() 

283 self._release_clause = tuple(int(v) for v in groups[1].split('.')) 

284 return result 

285 

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

287 

288 @property 

289 def is_prerelease(self): 

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

291 

292 

293def _match_prefix(x, y): 

294 x = str(x) 

295 y = str(y) 

296 if x == y: 

297 return True 

298 if not x.startswith(y): 

299 return False 

300 n = len(y) 

301 return x[n] == '.' 

302 

303 

304class NormalizedMatcher(Matcher): 

305 version_class = NormalizedVersion 

306 

307 # value is either a callable or the name of a method 

308 _operators = { 

309 '~=': '_match_compatible', 

310 '<': '_match_lt', 

311 '>': '_match_gt', 

312 '<=': '_match_le', 

313 '>=': '_match_ge', 

314 '==': '_match_eq', 

315 '===': '_match_arbitrary', 

316 '!=': '_match_ne', 

317 } 

318 

319 def _adjust_local(self, version, constraint, prefix): 

320 if prefix: 

321 strip_local = '+' not in constraint and version._parts[-1] 

322 else: 

323 # both constraint and version are 

324 # NormalizedVersion instances. 

325 # If constraint does not have a local component, 

326 # ensure the version doesn't, either. 

327 strip_local = not constraint._parts[-1] and version._parts[-1] 

328 if strip_local: 

329 s = version._string.split('+', 1)[0] 

330 version = self.version_class(s) 

331 return version, constraint 

332 

333 def _match_lt(self, version, constraint, prefix): 

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

335 if version >= constraint: 

336 return False 

337 release_clause = constraint._release_clause 

338 pfx = '.'.join([str(i) for i in release_clause]) 

339 return not _match_prefix(version, pfx) 

340 

341 def _match_gt(self, version, constraint, prefix): 

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

343 if version <= constraint: 

344 return False 

345 release_clause = constraint._release_clause 

346 pfx = '.'.join([str(i) for i in release_clause]) 

347 return not _match_prefix(version, pfx) 

348 

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

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

351 return version <= constraint 

352 

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

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

355 return version >= constraint 

356 

357 def _match_eq(self, version, constraint, prefix): 

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

359 if not prefix: 

360 result = (version == constraint) 

361 else: 

362 result = _match_prefix(version, constraint) 

363 return result 

364 

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

366 return str(version) == str(constraint) 

367 

368 def _match_ne(self, version, constraint, prefix): 

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

370 if not prefix: 

371 result = (version != constraint) 

372 else: 

373 result = not _match_prefix(version, constraint) 

374 return result 

375 

376 def _match_compatible(self, version, constraint, prefix): 

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

378 if version == constraint: 

379 return True 

380 if version < constraint: 

381 return False 

382# if not prefix: 

383# return True 

384 release_clause = constraint._release_clause 

385 if len(release_clause) > 1: 

386 release_clause = release_clause[:-1] 

387 pfx = '.'.join([str(i) for i in release_clause]) 

388 return _match_prefix(version, pfx) 

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_VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I) 

576_VERSION_REPLACE = { 

577 'pre': 'c', 

578 'preview': 'c', 

579 '-': 'final-', 

580 'rc': 'c', 

581 'dev': '@', 

582 '': None, 

583 '.': None, 

584} 

585 

586 

587def _legacy_key(s): 

588 def get_parts(s): 

589 result = [] 

590 for p in _VERSION_PART.split(s.lower()): 

591 p = _VERSION_REPLACE.get(p, p) 

592 if p: 

593 if '0' <= p[:1] <= '9': 

594 p = p.zfill(8) 

595 else: 

596 p = '*' + p 

597 result.append(p) 

598 result.append('*final') 

599 return result 

600 

601 result = [] 

602 for p in get_parts(s): 

603 if p.startswith('*'): 

604 if p < '*final': 

605 while result and result[-1] == '*final-': 

606 result.pop() 

607 while result and result[-1] == '00000000': 

608 result.pop() 

609 result.append(p) 

610 return tuple(result) 

611 

612 

613class LegacyVersion(Version): 

614 def parse(self, s): 

615 return _legacy_key(s) 

616 

617 @property 

618 def is_prerelease(self): 

619 result = False 

620 for x in self._parts: 

621 if (isinstance(x, string_types) and x.startswith('*') and 

622 x < '*final'): 

623 result = True 

624 break 

625 return result 

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_SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)' 

654 r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?' 

655 r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I) 

656 

657 

658def is_semver(s): 

659 return _SEMVER_RE.match(s) 

660 

661 

662def _semantic_key(s): 

663 def make_tuple(s, absent): 

664 if s is None: 

665 result = (absent,) 

666 else: 

667 parts = s[1:].split('.') 

668 # We can't compare ints and strings on Python 3, so fudge it 

669 # by zero-filling numeric values so simulate a numeric comparison 

670 result = tuple([p.zfill(8) if p.isdigit() else p for p in parts]) 

671 return result 

672 

673 m = is_semver(s) 

674 if not m: 

675 raise UnsupportedVersionError(s) 

676 groups = m.groups() 

677 major, minor, patch = [int(i) for i in groups[:3]] 

678 # choose the '|' and '*' so that versions sort correctly 

679 pre, build = make_tuple(groups[3], '|'), make_tuple(groups[5], '*') 

680 return (major, minor, patch), pre, build 

681 

682 

683class SemanticVersion(Version): 

684 def parse(self, s): 

685 return _semantic_key(s) 

686 

687 @property 

688 def is_prerelease(self): 

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

690 

691 

692class SemanticMatcher(Matcher): 

693 version_class = SemanticVersion 

694 

695 

696class VersionScheme(object): 

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

698 self.key = key 

699 self.matcher = matcher 

700 self.suggester = suggester 

701 

702 def is_valid_version(self, s): 

703 try: 

704 self.matcher.version_class(s) 

705 result = True 

706 except UnsupportedVersionError: 

707 result = False 

708 return result 

709 

710 def is_valid_matcher(self, s): 

711 try: 

712 self.matcher(s) 

713 result = True 

714 except UnsupportedVersionError: 

715 result = False 

716 return result 

717 

718 def is_valid_constraint_list(self, s): 

719 """ 

720 Used for processing some metadata fields 

721 """ 

722 # See issue #140. Be tolerant of a single trailing comma. 

723 if s.endswith(','): 

724 s = s[:-1] 

725 return self.is_valid_matcher('dummy_name (%s)' % s) 

726 

727 def suggest(self, s): 

728 if self.suggester is None: 

729 result = None 

730 else: 

731 result = self.suggester(s) 

732 return result 

733 

734_SCHEMES = { 

735 'normalized': VersionScheme(_normalized_key, NormalizedMatcher, 

736 _suggest_normalized_version), 

737 'legacy': VersionScheme(_legacy_key, LegacyMatcher, lambda self, s: s), 

738 'semantic': VersionScheme(_semantic_key, SemanticMatcher, 

739 _suggest_semantic_version), 

740} 

741 

742_SCHEMES['default'] = _SCHEMES['normalized'] 

743 

744 

745def get_scheme(name): 

746 if name not in _SCHEMES: 

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

748 return _SCHEMES[name]