Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/specifiers.py: 27%

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

291 statements  

1# This file is dual licensed under the terms of the Apache License, Version 

2# 2.0, and the BSD License. See the LICENSE file in the root of this repository 

3# for complete details. 

4""" 

5.. testsetup:: 

6 

7 from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier 

8 from packaging.version import Version 

9""" 

10 

11from __future__ import annotations 

12 

13import abc 

14import itertools 

15import re 

16from typing import Callable, Final, Iterable, Iterator, TypeVar, Union 

17 

18from .utils import canonicalize_version 

19from .version import InvalidVersion, Version 

20 

21UnparsedVersion = Union[Version, str] 

22UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) 

23CallableOperator = Callable[[Version, str], bool] 

24 

25 

26def _coerce_version(version: UnparsedVersion) -> Version | None: 

27 if not isinstance(version, Version): 

28 try: 

29 version = Version(version) 

30 except InvalidVersion: 

31 return None 

32 return version 

33 

34 

35def _public_version(version: Version) -> Version: 

36 return version.__replace__(local=None) 

37 

38 

39def _base_version(version: Version) -> Version: 

40 return version.__replace__(pre=None, post=None, dev=None, local=None) 

41 

42 

43class InvalidSpecifier(ValueError): 

44 """ 

45 Raised when attempting to create a :class:`Specifier` with a specifier 

46 string that is invalid. 

47 

48 >>> Specifier("lolwat") 

49 Traceback (most recent call last): 

50 ... 

51 packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat' 

52 """ 

53 

54 

55class BaseSpecifier(metaclass=abc.ABCMeta): 

56 __slots__ = () 

57 __match_args__ = ("_str",) 

58 

59 @property 

60 def _str(self) -> str: 

61 """Internal property for match_args""" 

62 return str(self) 

63 

64 @abc.abstractmethod 

65 def __str__(self) -> str: 

66 """ 

67 Returns the str representation of this Specifier-like object. This 

68 should be representative of the Specifier itself. 

69 """ 

70 

71 @abc.abstractmethod 

72 def __hash__(self) -> int: 

73 """ 

74 Returns a hash value for this Specifier-like object. 

75 """ 

76 

77 @abc.abstractmethod 

78 def __eq__(self, other: object) -> bool: 

79 """ 

80 Returns a boolean representing whether or not the two Specifier-like 

81 objects are equal. 

82 

83 :param other: The other object to check against. 

84 """ 

85 

86 @property 

87 @abc.abstractmethod 

88 def prereleases(self) -> bool | None: 

89 """Whether or not pre-releases as a whole are allowed. 

90 

91 This can be set to either ``True`` or ``False`` to explicitly enable or disable 

92 prereleases or it can be set to ``None`` (the default) to use default semantics. 

93 """ 

94 

95 @prereleases.setter # noqa: B027 

96 def prereleases(self, value: bool) -> None: 

97 """Setter for :attr:`prereleases`. 

98 

99 :param value: The value to set. 

100 """ 

101 

102 @abc.abstractmethod 

103 def contains(self, item: str, prereleases: bool | None = None) -> bool: 

104 """ 

105 Determines if the given item is contained within this specifier. 

106 """ 

107 

108 @abc.abstractmethod 

109 def filter( 

110 self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None 

111 ) -> Iterator[UnparsedVersionVar]: 

112 """ 

113 Takes an iterable of items and filters them so that only items which 

114 are contained within this specifier are allowed in it. 

115 """ 

116 

117 

118class Specifier(BaseSpecifier): 

119 """This class abstracts handling of version specifiers. 

120 

121 .. tip:: 

122 

123 It is generally not required to instantiate this manually. You should instead 

124 prefer to work with :class:`SpecifierSet` instead, which can parse 

125 comma-separated version specifiers (which is what package metadata contains). 

126 """ 

127 

128 __slots__ = ("_prereleases", "_spec", "_spec_version") 

129 

130 _operator_regex_str = r""" 

131 (?P<operator>(~=|==|!=|<=|>=|<|>|===)) 

132 """ 

133 _version_regex_str = r""" 

134 (?P<version> 

135 (?: 

136 # The identity operators allow for an escape hatch that will 

137 # do an exact string match of the version you wish to install. 

138 # This will not be parsed by PEP 440 and we cannot determine 

139 # any semantic meaning from it. This operator is discouraged 

140 # but included entirely as an escape hatch. 

141 (?<====) # Only match for the identity operator 

142 \s* 

143 [^\s;)]* # The arbitrary version can be just about anything, 

144 # we match everything except for whitespace, a 

145 # semi-colon for marker support, and a closing paren 

146 # since versions can be enclosed in them. 

147 ) 

148 | 

149 (?: 

150 # The (non)equality operators allow for wild card and local 

151 # versions to be specified so we have to define these two 

152 # operators separately to enable that. 

153 (?<===|!=) # Only match for equals and not equals 

154 

155 \s* 

156 v? 

157 (?:[0-9]+!)? # epoch 

158 [0-9]+(?:\.[0-9]+)* # release 

159 

160 # You cannot use a wild card and a pre-release, post-release, a dev or 

161 # local version together so group them with a | and make them optional. 

162 (?: 

163 \.\* # Wild card syntax of .* 

164 | 

165 (?: # pre release 

166 [-_\.]? 

167 (alpha|beta|preview|pre|a|b|c|rc) 

168 [-_\.]? 

169 [0-9]* 

170 )? 

171 (?: # post release 

172 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) 

173 )? 

174 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release 

175 (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local 

176 )? 

177 ) 

178 | 

179 (?: 

180 # The compatible operator requires at least two digits in the 

181 # release segment. 

182 (?<=~=) # Only match for the compatible operator 

183 

184 \s* 

185 v? 

186 (?:[0-9]+!)? # epoch 

187 [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *) 

188 (?: # pre release 

189 [-_\.]? 

190 (alpha|beta|preview|pre|a|b|c|rc) 

191 [-_\.]? 

192 [0-9]* 

193 )? 

194 (?: # post release 

195 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) 

196 )? 

197 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release 

198 ) 

199 | 

200 (?: 

201 # All other operators only allow a sub set of what the 

202 # (non)equality operators do. Specifically they do not allow 

203 # local versions to be specified nor do they allow the prefix 

204 # matching wild cards. 

205 (?<!==|!=|~=) # We have special cases for these 

206 # operators so we want to make sure they 

207 # don't match here. 

208 

209 \s* 

210 v? 

211 (?:[0-9]+!)? # epoch 

212 [0-9]+(?:\.[0-9]+)* # release 

213 (?: # pre release 

214 [-_\.]? 

215 (alpha|beta|preview|pre|a|b|c|rc) 

216 [-_\.]? 

217 [0-9]* 

218 )? 

219 (?: # post release 

220 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) 

221 )? 

222 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release 

223 ) 

224 ) 

225 """ 

226 

227 _regex = re.compile( 

228 r"\s*" + _operator_regex_str + _version_regex_str + r"\s*", 

229 re.VERBOSE | re.IGNORECASE, 

230 ) 

231 

232 _operators: Final = { 

233 "~=": "compatible", 

234 "==": "equal", 

235 "!=": "not_equal", 

236 "<=": "less_than_equal", 

237 ">=": "greater_than_equal", 

238 "<": "less_than", 

239 ">": "greater_than", 

240 "===": "arbitrary", 

241 } 

242 

243 def __init__(self, spec: str = "", prereleases: bool | None = None) -> None: 

244 """Initialize a Specifier instance. 

245 

246 :param spec: 

247 The string representation of a specifier which will be parsed and 

248 normalized before use. 

249 :param prereleases: 

250 This tells the specifier if it should accept prerelease versions if 

251 applicable or not. The default of ``None`` will autodetect it from the 

252 given specifiers. 

253 :raises InvalidSpecifier: 

254 If the given specifier is invalid (i.e. bad syntax). 

255 """ 

256 match = self._regex.fullmatch(spec) 

257 if not match: 

258 raise InvalidSpecifier(f"Invalid specifier: {spec!r}") 

259 

260 self._spec: tuple[str, str] = ( 

261 match.group("operator").strip(), 

262 match.group("version").strip(), 

263 ) 

264 

265 # Store whether or not this Specifier should accept prereleases 

266 self._prereleases = prereleases 

267 

268 # Specifier version cache 

269 self._spec_version: tuple[str, Version] | None = None 

270 

271 def _get_spec_version(self, version: str) -> Version | None: 

272 """One element cache, as only one spec Version is needed per Specifier.""" 

273 if self._spec_version is not None and self._spec_version[0] == version: 

274 return self._spec_version[1] 

275 

276 version_specifier = _coerce_version(version) 

277 if version_specifier is None: 

278 return None 

279 

280 self._spec_version = (version, version_specifier) 

281 return version_specifier 

282 

283 def _require_spec_version(self, version: str) -> Version: 

284 """Get spec version, asserting it's valid (not for === operator). 

285 

286 This method should only be called for operators where version 

287 strings are guaranteed to be valid PEP 440 versions (not ===). 

288 """ 

289 spec_version = self._get_spec_version(version) 

290 assert spec_version is not None 

291 return spec_version 

292 

293 @property 

294 def prereleases(self) -> bool | None: 

295 # If there is an explicit prereleases set for this, then we'll just 

296 # blindly use that. 

297 if self._prereleases is not None: 

298 return self._prereleases 

299 

300 # Only the "!=" operator does not imply prereleases when 

301 # the version in the specifier is a prerelease. 

302 operator, version_str = self._spec 

303 if operator != "!=": 

304 # The == specifier with trailing .* cannot include prereleases 

305 # e.g. "==1.0a1.*" is not valid. 

306 if operator == "==" and version_str.endswith(".*"): 

307 return False 

308 

309 # "===" can have arbitrary string versions, so we cannot parse 

310 # those, we take prereleases as unknown (None) for those. 

311 version = self._get_spec_version(version_str) 

312 if version is None: 

313 return None 

314 

315 # For all other operators, use the check if spec Version 

316 # object implies pre-releases. 

317 if version.is_prerelease: 

318 return True 

319 

320 return False 

321 

322 @prereleases.setter 

323 def prereleases(self, value: bool | None) -> None: 

324 self._prereleases = value 

325 

326 @property 

327 def operator(self) -> str: 

328 """The operator of this specifier. 

329 

330 >>> Specifier("==1.2.3").operator 

331 '==' 

332 """ 

333 return self._spec[0] 

334 

335 @property 

336 def version(self) -> str: 

337 """The version of this specifier. 

338 

339 >>> Specifier("==1.2.3").version 

340 '1.2.3' 

341 """ 

342 return self._spec[1] 

343 

344 def __repr__(self) -> str: 

345 """A representation of the Specifier that shows all internal state. 

346 

347 >>> Specifier('>=1.0.0') 

348 <Specifier('>=1.0.0')> 

349 >>> Specifier('>=1.0.0', prereleases=False) 

350 <Specifier('>=1.0.0', prereleases=False)> 

351 >>> Specifier('>=1.0.0', prereleases=True) 

352 <Specifier('>=1.0.0', prereleases=True)> 

353 """ 

354 pre = ( 

355 f", prereleases={self.prereleases!r}" 

356 if self._prereleases is not None 

357 else "" 

358 ) 

359 

360 return f"<{self.__class__.__name__}({str(self)!r}{pre})>" 

361 

362 def __str__(self) -> str: 

363 """A string representation of the Specifier that can be round-tripped. 

364 

365 >>> str(Specifier('>=1.0.0')) 

366 '>=1.0.0' 

367 >>> str(Specifier('>=1.0.0', prereleases=False)) 

368 '>=1.0.0' 

369 """ 

370 return "{}{}".format(*self._spec) 

371 

372 @property 

373 def _canonical_spec(self) -> tuple[str, str]: 

374 operator, version = self._spec 

375 if operator == "===" or version.endswith(".*"): 

376 return operator, version 

377 

378 spec_version = self._require_spec_version(version) 

379 

380 canonical_version = canonicalize_version( 

381 spec_version, strip_trailing_zero=(operator != "~=") 

382 ) 

383 

384 return operator, canonical_version 

385 

386 def __hash__(self) -> int: 

387 return hash(self._canonical_spec) 

388 

389 def __eq__(self, other: object) -> bool: 

390 """Whether or not the two Specifier-like objects are equal. 

391 

392 :param other: The other object to check against. 

393 

394 The value of :attr:`prereleases` is ignored. 

395 

396 >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0") 

397 True 

398 >>> (Specifier("==1.2.3", prereleases=False) == 

399 ... Specifier("==1.2.3", prereleases=True)) 

400 True 

401 >>> Specifier("==1.2.3") == "==1.2.3" 

402 True 

403 >>> Specifier("==1.2.3") == Specifier("==1.2.4") 

404 False 

405 >>> Specifier("==1.2.3") == Specifier("~=1.2.3") 

406 False 

407 """ 

408 if isinstance(other, str): 

409 try: 

410 other = self.__class__(str(other)) 

411 except InvalidSpecifier: 

412 return NotImplemented 

413 elif not isinstance(other, self.__class__): 

414 return NotImplemented 

415 

416 return self._canonical_spec == other._canonical_spec 

417 

418 def _get_operator(self, op: str) -> CallableOperator: 

419 operator_callable: CallableOperator = getattr( 

420 self, f"_compare_{self._operators[op]}" 

421 ) 

422 return operator_callable 

423 

424 def _compare_compatible(self, prospective: Version, spec: str) -> bool: 

425 # Compatible releases have an equivalent combination of >= and ==. That 

426 # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to 

427 # implement this in terms of the other specifiers instead of 

428 # implementing it ourselves. The only thing we need to do is construct 

429 # the other specifiers. 

430 

431 # We want everything but the last item in the version, but we want to 

432 # ignore suffix segments. 

433 prefix = _version_join( 

434 list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] 

435 ) 

436 

437 # Add the prefix notation to the end of our string 

438 prefix += ".*" 

439 

440 return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( 

441 prospective, prefix 

442 ) 

443 

444 def _compare_equal(self, prospective: Version, spec: str) -> bool: 

445 # We need special logic to handle prefix matching 

446 if spec.endswith(".*"): 

447 # In the case of prefix matching we want to ignore local segment. 

448 normalized_prospective = canonicalize_version( 

449 _public_version(prospective), strip_trailing_zero=False 

450 ) 

451 # Get the normalized version string ignoring the trailing .* 

452 normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) 

453 # Split the spec out by bangs and dots, and pretend that there is 

454 # an implicit dot in between a release segment and a pre-release segment. 

455 split_spec = _version_split(normalized_spec) 

456 

457 # Split the prospective version out by bangs and dots, and pretend 

458 # that there is an implicit dot in between a release segment and 

459 # a pre-release segment. 

460 split_prospective = _version_split(normalized_prospective) 

461 

462 # 0-pad the prospective version before shortening it to get the correct 

463 # shortened version. 

464 padded_prospective, _ = _pad_version(split_prospective, split_spec) 

465 

466 # Shorten the prospective version to be the same length as the spec 

467 # so that we can determine if the specifier is a prefix of the 

468 # prospective version or not. 

469 shortened_prospective = padded_prospective[: len(split_spec)] 

470 

471 return shortened_prospective == split_spec 

472 else: 

473 # Convert our spec string into a Version 

474 spec_version = self._require_spec_version(spec) 

475 

476 # If the specifier does not have a local segment, then we want to 

477 # act as if the prospective version also does not have a local 

478 # segment. 

479 if not spec_version.local: 

480 prospective = _public_version(prospective) 

481 

482 return prospective == spec_version 

483 

484 def _compare_not_equal(self, prospective: Version, spec: str) -> bool: 

485 return not self._compare_equal(prospective, spec) 

486 

487 def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: 

488 # NB: Local version identifiers are NOT permitted in the version 

489 # specifier, so local version labels can be universally removed from 

490 # the prospective version. 

491 return _public_version(prospective) <= self._require_spec_version(spec) 

492 

493 def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: 

494 # NB: Local version identifiers are NOT permitted in the version 

495 # specifier, so local version labels can be universally removed from 

496 # the prospective version. 

497 return _public_version(prospective) >= self._require_spec_version(spec) 

498 

499 def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: 

500 # Convert our spec to a Version instance, since we'll want to work with 

501 # it as a version. 

502 spec = self._require_spec_version(spec_str) 

503 

504 # Check to see if the prospective version is less than the spec 

505 # version. If it's not we can short circuit and just return False now 

506 # instead of doing extra unneeded work. 

507 if not prospective < spec: 

508 return False 

509 

510 # This special case is here so that, unless the specifier itself 

511 # includes is a pre-release version, that we do not accept pre-release 

512 # versions for the version mentioned in the specifier (e.g. <3.1 should 

513 # not match 3.1.dev0, but should match 3.0.dev0). 

514 if ( 

515 not spec.is_prerelease 

516 and prospective.is_prerelease 

517 and _base_version(prospective) == _base_version(spec) 

518 ): 

519 return False 

520 

521 # If we've gotten to here, it means that prospective version is both 

522 # less than the spec version *and* it's not a pre-release of the same 

523 # version in the spec. 

524 return True 

525 

526 def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: 

527 # Convert our spec to a Version instance, since we'll want to work with 

528 # it as a version. 

529 spec = self._require_spec_version(spec_str) 

530 

531 # Check to see if the prospective version is greater than the spec 

532 # version. If it's not we can short circuit and just return False now 

533 # instead of doing extra unneeded work. 

534 if not prospective > spec: 

535 return False 

536 

537 # This special case is here so that, unless the specifier itself 

538 # includes is a post-release version, that we do not accept 

539 # post-release versions for the version mentioned in the specifier 

540 # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). 

541 if ( 

542 not spec.is_postrelease 

543 and prospective.is_postrelease 

544 and _base_version(prospective) == _base_version(spec) 

545 ): 

546 return False 

547 

548 # Ensure that we do not allow a local version of the version mentioned 

549 # in the specifier, which is technically greater than, to match. 

550 if prospective.local is not None and _base_version( 

551 prospective 

552 ) == _base_version(spec): 

553 return False 

554 

555 # If we've gotten to here, it means that prospective version is both 

556 # greater than the spec version *and* it's not a pre-release of the 

557 # same version in the spec. 

558 return True 

559 

560 def _compare_arbitrary(self, prospective: Version | str, spec: str) -> bool: 

561 return str(prospective).lower() == str(spec).lower() 

562 

563 def __contains__(self, item: str | Version) -> bool: 

564 """Return whether or not the item is contained in this specifier. 

565 

566 :param item: The item to check for. 

567 

568 This is used for the ``in`` operator and behaves the same as 

569 :meth:`contains` with no ``prereleases`` argument passed. 

570 

571 >>> "1.2.3" in Specifier(">=1.2.3") 

572 True 

573 >>> Version("1.2.3") in Specifier(">=1.2.3") 

574 True 

575 >>> "1.0.0" in Specifier(">=1.2.3") 

576 False 

577 >>> "1.3.0a1" in Specifier(">=1.2.3") 

578 True 

579 >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True) 

580 True 

581 """ 

582 return self.contains(item) 

583 

584 def contains(self, item: UnparsedVersion, prereleases: bool | None = None) -> bool: 

585 """Return whether or not the item is contained in this specifier. 

586 

587 :param item: 

588 The item to check for, which can be a version string or a 

589 :class:`Version` instance. 

590 :param prereleases: 

591 Whether or not to match prereleases with this Specifier. If set to 

592 ``None`` (the default), it will follow the recommendation from 

593 :pep:`440` and match prereleases, as there are no other versions. 

594 

595 >>> Specifier(">=1.2.3").contains("1.2.3") 

596 True 

597 >>> Specifier(">=1.2.3").contains(Version("1.2.3")) 

598 True 

599 >>> Specifier(">=1.2.3").contains("1.0.0") 

600 False 

601 >>> Specifier(">=1.2.3").contains("1.3.0a1") 

602 True 

603 >>> Specifier(">=1.2.3", prereleases=False).contains("1.3.0a1") 

604 False 

605 >>> Specifier(">=1.2.3").contains("1.3.0a1") 

606 True 

607 """ 

608 

609 return bool(list(self.filter([item], prereleases=prereleases))) 

610 

611 def filter( 

612 self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None 

613 ) -> Iterator[UnparsedVersionVar]: 

614 """Filter items in the given iterable, that match the specifier. 

615 

616 :param iterable: 

617 An iterable that can contain version strings and :class:`Version` instances. 

618 The items in the iterable will be filtered according to the specifier. 

619 :param prereleases: 

620 Whether or not to allow prereleases in the returned iterator. If set to 

621 ``None`` (the default), it will follow the recommendation from :pep:`440` 

622 and match prereleases if there are no other versions. 

623 

624 >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) 

625 ['1.3'] 

626 >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")])) 

627 ['1.2.3', '1.3', <Version('1.4')>] 

628 >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"])) 

629 ['1.5a1'] 

630 >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) 

631 ['1.3', '1.5a1'] 

632 >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) 

633 ['1.3', '1.5a1'] 

634 """ 

635 prereleases_versions = [] 

636 found_non_prereleases = False 

637 

638 # Determine if to include prereleases by default 

639 include_prereleases = ( 

640 prereleases if prereleases is not None else self.prereleases 

641 ) 

642 

643 # Get the matching operator 

644 operator_callable = self._get_operator(self.operator) 

645 

646 # Filter versions 

647 for version in iterable: 

648 parsed_version = _coerce_version(version) 

649 if parsed_version is None: 

650 # === operator can match arbitrary (non-version) strings 

651 if self.operator == "===" and self._compare_arbitrary( 

652 version, self.version 

653 ): 

654 yield version 

655 elif operator_callable(parsed_version, self.version): 

656 # If it's not a prerelease or prereleases are allowed, yield it directly 

657 if not parsed_version.is_prerelease or include_prereleases: 

658 found_non_prereleases = True 

659 yield version 

660 # Otherwise collect prereleases for potential later use 

661 elif prereleases is None and self._prereleases is not False: 

662 prereleases_versions.append(version) 

663 

664 # If no non-prereleases were found and prereleases weren't 

665 # explicitly forbidden, yield the collected prereleases 

666 if ( 

667 not found_non_prereleases 

668 and prereleases is None 

669 and self._prereleases is not False 

670 ): 

671 yield from prereleases_versions 

672 

673 

674_prefix_regex = re.compile(r"([0-9]+)((?:a|b|c|rc)[0-9]+)") 

675 

676 

677def _version_split(version: str) -> list[str]: 

678 """Split version into components. 

679 

680 The split components are intended for version comparison. The logic does 

681 not attempt to retain the original version string, so joining the 

682 components back with :func:`_version_join` may not produce the original 

683 version string. 

684 """ 

685 result: list[str] = [] 

686 

687 epoch, _, rest = version.rpartition("!") 

688 result.append(epoch or "0") 

689 

690 for item in rest.split("."): 

691 match = _prefix_regex.fullmatch(item) 

692 if match: 

693 result.extend(match.groups()) 

694 else: 

695 result.append(item) 

696 return result 

697 

698 

699def _version_join(components: list[str]) -> str: 

700 """Join split version components into a version string. 

701 

702 This function assumes the input came from :func:`_version_split`, where the 

703 first component must be the epoch (either empty or numeric), and all other 

704 components numeric. 

705 """ 

706 epoch, *rest = components 

707 return f"{epoch}!{'.'.join(rest)}" 

708 

709 

710def _is_not_suffix(segment: str) -> bool: 

711 return not any( 

712 segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") 

713 ) 

714 

715 

716def _pad_version(left: list[str], right: list[str]) -> tuple[list[str], list[str]]: 

717 left_split, right_split = [], [] 

718 

719 # Get the release segment of our versions 

720 left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left))) 

721 right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) 

722 

723 # Get the rest of our versions 

724 left_split.append(left[len(left_split[0]) :]) 

725 right_split.append(right[len(right_split[0]) :]) 

726 

727 # Insert our padding 

728 left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) 

729 right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) 

730 

731 return ( 

732 list(itertools.chain.from_iterable(left_split)), 

733 list(itertools.chain.from_iterable(right_split)), 

734 ) 

735 

736 

737class SpecifierSet(BaseSpecifier): 

738 """This class abstracts handling of a set of version specifiers. 

739 

740 It can be passed a single specifier (``>=3.0``), a comma-separated list of 

741 specifiers (``>=3.0,!=3.1``), or no specifier at all. 

742 """ 

743 

744 __slots__ = ("_prereleases", "_specs") 

745 

746 def __init__( 

747 self, 

748 specifiers: str | Iterable[Specifier] = "", 

749 prereleases: bool | None = None, 

750 ) -> None: 

751 """Initialize a SpecifierSet instance. 

752 

753 :param specifiers: 

754 The string representation of a specifier or a comma-separated list of 

755 specifiers which will be parsed and normalized before use. 

756 May also be an iterable of ``Specifier`` instances, which will be used 

757 as is. 

758 :param prereleases: 

759 This tells the SpecifierSet if it should accept prerelease versions if 

760 applicable or not. The default of ``None`` will autodetect it from the 

761 given specifiers. 

762 

763 :raises InvalidSpecifier: 

764 If the given ``specifiers`` are not parseable than this exception will be 

765 raised. 

766 """ 

767 

768 if isinstance(specifiers, str): 

769 # Split on `,` to break each individual specifier into its own item, and 

770 # strip each item to remove leading/trailing whitespace. 

771 split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] 

772 

773 # Make each individual specifier a Specifier and save in a frozen set 

774 # for later. 

775 self._specs = frozenset(map(Specifier, split_specifiers)) 

776 else: 

777 # Save the supplied specifiers in a frozen set. 

778 self._specs = frozenset(specifiers) 

779 

780 # Store our prereleases value so we can use it later to determine if 

781 # we accept prereleases or not. 

782 self._prereleases = prereleases 

783 

784 @property 

785 def prereleases(self) -> bool | None: 

786 # If we have been given an explicit prerelease modifier, then we'll 

787 # pass that through here. 

788 if self._prereleases is not None: 

789 return self._prereleases 

790 

791 # If we don't have any specifiers, and we don't have a forced value, 

792 # then we'll just return None since we don't know if this should have 

793 # pre-releases or not. 

794 if not self._specs: 

795 return None 

796 

797 # Otherwise we'll see if any of the given specifiers accept 

798 # prereleases, if any of them do we'll return True, otherwise False. 

799 if any(s.prereleases for s in self._specs): 

800 return True 

801 

802 return None 

803 

804 @prereleases.setter 

805 def prereleases(self, value: bool | None) -> None: 

806 self._prereleases = value 

807 

808 def __repr__(self) -> str: 

809 """A representation of the specifier set that shows all internal state. 

810 

811 Note that the ordering of the individual specifiers within the set may not 

812 match the input string. 

813 

814 >>> SpecifierSet('>=1.0.0,!=2.0.0') 

815 <SpecifierSet('!=2.0.0,>=1.0.0')> 

816 >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False) 

817 <SpecifierSet('!=2.0.0,>=1.0.0', prereleases=False)> 

818 >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True) 

819 <SpecifierSet('!=2.0.0,>=1.0.0', prereleases=True)> 

820 """ 

821 pre = ( 

822 f", prereleases={self.prereleases!r}" 

823 if self._prereleases is not None 

824 else "" 

825 ) 

826 

827 return f"<SpecifierSet({str(self)!r}{pre})>" 

828 

829 def __str__(self) -> str: 

830 """A string representation of the specifier set that can be round-tripped. 

831 

832 Note that the ordering of the individual specifiers within the set may not 

833 match the input string. 

834 

835 >>> str(SpecifierSet(">=1.0.0,!=1.0.1")) 

836 '!=1.0.1,>=1.0.0' 

837 >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False)) 

838 '!=1.0.1,>=1.0.0' 

839 """ 

840 return ",".join(sorted(str(s) for s in self._specs)) 

841 

842 def __hash__(self) -> int: 

843 return hash(self._specs) 

844 

845 def __and__(self, other: SpecifierSet | str) -> SpecifierSet: 

846 """Return a SpecifierSet which is a combination of the two sets. 

847 

848 :param other: The other object to combine with. 

849 

850 >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1' 

851 <SpecifierSet('!=1.0.1,!=2.0.1,<=2.0.0,>=1.0.0')> 

852 >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1') 

853 <SpecifierSet('!=1.0.1,!=2.0.1,<=2.0.0,>=1.0.0')> 

854 """ 

855 if isinstance(other, str): 

856 other = SpecifierSet(other) 

857 elif not isinstance(other, SpecifierSet): 

858 return NotImplemented 

859 

860 specifier = SpecifierSet() 

861 specifier._specs = frozenset(self._specs | other._specs) 

862 

863 if self._prereleases is None and other._prereleases is not None: 

864 specifier._prereleases = other._prereleases 

865 elif ( 

866 self._prereleases is not None and other._prereleases is None 

867 ) or self._prereleases == other._prereleases: 

868 specifier._prereleases = self._prereleases 

869 else: 

870 raise ValueError( 

871 "Cannot combine SpecifierSets with True and False prerelease overrides." 

872 ) 

873 

874 return specifier 

875 

876 def __eq__(self, other: object) -> bool: 

877 """Whether or not the two SpecifierSet-like objects are equal. 

878 

879 :param other: The other object to check against. 

880 

881 The value of :attr:`prereleases` is ignored. 

882 

883 >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1") 

884 True 

885 >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) == 

886 ... SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)) 

887 True 

888 >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1" 

889 True 

890 >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0") 

891 False 

892 >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2") 

893 False 

894 """ 

895 if isinstance(other, (str, Specifier)): 

896 other = SpecifierSet(str(other)) 

897 elif not isinstance(other, SpecifierSet): 

898 return NotImplemented 

899 

900 return self._specs == other._specs 

901 

902 def __len__(self) -> int: 

903 """Returns the number of specifiers in this specifier set.""" 

904 return len(self._specs) 

905 

906 def __iter__(self) -> Iterator[Specifier]: 

907 """ 

908 Returns an iterator over all the underlying :class:`Specifier` instances 

909 in this specifier set. 

910 

911 >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str) 

912 [<Specifier('!=1.0.1')>, <Specifier('>=1.0.0')>] 

913 """ 

914 return iter(self._specs) 

915 

916 def __contains__(self, item: UnparsedVersion) -> bool: 

917 """Return whether or not the item is contained in this specifier. 

918 

919 :param item: The item to check for. 

920 

921 This is used for the ``in`` operator and behaves the same as 

922 :meth:`contains` with no ``prereleases`` argument passed. 

923 

924 >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1") 

925 True 

926 >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1") 

927 True 

928 >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1") 

929 False 

930 >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1") 

931 True 

932 >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True) 

933 True 

934 """ 

935 return self.contains(item) 

936 

937 def contains( 

938 self, 

939 item: UnparsedVersion, 

940 prereleases: bool | None = None, 

941 installed: bool | None = None, 

942 ) -> bool: 

943 """Return whether or not the item is contained in this SpecifierSet. 

944 

945 :param item: 

946 The item to check for, which can be a version string or a 

947 :class:`Version` instance. 

948 :param prereleases: 

949 Whether or not to match prereleases with this SpecifierSet. If set to 

950 ``None`` (the default), it will follow the recommendation from :pep:`440` 

951 and match prereleases, as there are no other versions. 

952 :param installed: 

953 Whether or not the item is installed. If set to ``True``, it will 

954 accept prerelease versions even if the specifier does not allow them. 

955 

956 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3") 

957 True 

958 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3")) 

959 True 

960 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1") 

961 False 

962 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1") 

963 True 

964 >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False).contains("1.3.0a1") 

965 False 

966 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True) 

967 True 

968 """ 

969 version = _coerce_version(item) 

970 

971 if version is not None and installed and version.is_prerelease: 

972 prereleases = True 

973 

974 check_item = item if version is None else version 

975 return bool(list(self.filter([check_item], prereleases=prereleases))) 

976 

977 def filter( 

978 self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None 

979 ) -> Iterator[UnparsedVersionVar]: 

980 """Filter items in the given iterable, that match the specifiers in this set. 

981 

982 :param iterable: 

983 An iterable that can contain version strings and :class:`Version` instances. 

984 The items in the iterable will be filtered according to the specifier. 

985 :param prereleases: 

986 Whether or not to allow prereleases in the returned iterator. If set to 

987 ``None`` (the default), it will follow the recommendation from :pep:`440` 

988 and match prereleases if there are no other versions. 

989 

990 >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) 

991 ['1.3'] 

992 >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")])) 

993 ['1.3', <Version('1.4')>] 

994 >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"])) 

995 ['1.5a1'] 

996 >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) 

997 ['1.3', '1.5a1'] 

998 >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) 

999 ['1.3', '1.5a1'] 

1000 

1001 An "empty" SpecifierSet will filter items based on the presence of prerelease 

1002 versions in the set. 

1003 

1004 >>> list(SpecifierSet("").filter(["1.3", "1.5a1"])) 

1005 ['1.3'] 

1006 >>> list(SpecifierSet("").filter(["1.5a1"])) 

1007 ['1.5a1'] 

1008 >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"])) 

1009 ['1.3', '1.5a1'] 

1010 >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True)) 

1011 ['1.3', '1.5a1'] 

1012 """ 

1013 # Determine if we're forcing a prerelease or not, if we're not forcing 

1014 # one for this particular filter call, then we'll use whatever the 

1015 # SpecifierSet thinks for whether or not we should support prereleases. 

1016 if prereleases is None and self.prereleases is not None: 

1017 prereleases = self.prereleases 

1018 

1019 # If we have any specifiers, then we want to wrap our iterable in the 

1020 # filter method for each one, this will act as a logical AND amongst 

1021 # each specifier. 

1022 if self._specs: 

1023 # When prereleases is None, we need to let all versions through 

1024 # the individual filters, then decide about prereleases at the end 

1025 # based on whether any non-prereleases matched ALL specs. 

1026 for spec in self._specs: 

1027 iterable = spec.filter( 

1028 iterable, prereleases=True if prereleases is None else prereleases 

1029 ) 

1030 

1031 if prereleases is not None: 

1032 # If we have a forced prereleases value, 

1033 # we can immediately return the iterator. 

1034 return iter(iterable) 

1035 else: 

1036 # Handle empty SpecifierSet cases where prereleases is not None. 

1037 if prereleases is True: 

1038 return iter(iterable) 

1039 

1040 if prereleases is False: 

1041 return ( 

1042 item 

1043 for item in iterable 

1044 if (version := _coerce_version(item)) is None 

1045 or not version.is_prerelease 

1046 ) 

1047 

1048 # Finally if prereleases is None, apply PEP 440 logic: 

1049 # exclude prereleases unless there are no final releases that matched. 

1050 filtered_items: list[UnparsedVersionVar] = [] 

1051 found_prereleases: list[UnparsedVersionVar] = [] 

1052 found_final_release = False 

1053 

1054 for item in iterable: 

1055 parsed_version = _coerce_version(item) 

1056 # Arbitrary strings are always included as it is not 

1057 # possible to determine if they are prereleases, 

1058 # and they have already passed all specifiers. 

1059 if parsed_version is None: 

1060 filtered_items.append(item) 

1061 found_prereleases.append(item) 

1062 elif parsed_version.is_prerelease: 

1063 found_prereleases.append(item) 

1064 else: 

1065 filtered_items.append(item) 

1066 found_final_release = True 

1067 

1068 return iter(filtered_items if found_final_release else found_prereleases)