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

397 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-17 07:30 +0000

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|b|c|rc)(\d+))?' 

180 r'(\.(post)(\d+))?(\.(dev)(\d+))?' 

181 r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$') 

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

248class NormalizedVersion(Version): 

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 

284def _match_prefix(x, y): 

285 x = str(x) 

286 y = str(y) 

287 if x == y: 

288 return True 

289 if not x.startswith(y): 

290 return False 

291 n = len(y) 

292 return x[n] == '.' 

293 

294 

295class NormalizedMatcher(Matcher): 

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 

406def _suggest_semantic_version(s): 

407 """ 

408 Try to suggest a semantic form for a version for which 

409 _suggest_normalized_version couldn't come up with anything. 

410 """ 

411 result = s.strip().lower() 

412 for pat, repl in _REPLACEMENTS: 

413 result = pat.sub(repl, result) 

414 if not result: 

415 result = '0.0.0' 

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 

452def _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 

578def _legacy_key(s): 

579 def get_parts(s): 

580 result = [] 

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

582 p = _VERSION_REPLACE.get(p, p) 

583 if p: 

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

585 p = p.zfill(8) 

586 else: 

587 p = '*' + p 

588 result.append(p) 

589 result.append('*final') 

590 return result 

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 

604class LegacyVersion(Version): 

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 

619class LegacyMatcher(Matcher): 

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 

649def is_semver(s): 

650 return _SEMVER_RE.match(s) 

651 

652 

653def _semantic_key(s): 

654 def make_tuple(s, absent): 

655 if s is None: 

656 result = (absent,) 

657 else: 

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

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

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

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

662 return result 

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 

674class SemanticVersion(Version): 

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 

683class SemanticMatcher(Matcher): 

684 version_class = SemanticVersion 

685 

686 

687class VersionScheme(object): 

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 

736def get_scheme(name): 

737 if name not in _SCHEMES: 

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

739 return _SCHEMES[name]