codekingpro/portable-devtools
114k
1# This file is dual licensed under the terms of the Apache License, Version2# 2.0, and the BSD License. See the LICENSE file in the root of this repository3# for complete details.4"""5.. testsetup::6 7 from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier8 from packaging.version import Version9"""10 11from __future__ import annotations12 13import abc14import itertools15import re16from typing import Callable, Final, Iterable, Iterator, TypeVar, Union17 18from .utils import canonicalize_version19from .version import InvalidVersion, Version20 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 None32 return version33 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 specifier46 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 @property60 def _str(self) -> str:61 """Internal property for match_args"""62 return str(self)63 64 @abc.abstractmethod65 def __str__(self) -> str:66 """67 Returns the str representation of this Specifier-like object. This68 should be representative of the Specifier itself.69 """70 71 @abc.abstractmethod72 def __hash__(self) -> int:73 """74 Returns a hash value for this Specifier-like object.75 """76 77 @abc.abstractmethod78 def __eq__(self, other: object) -> bool:79 """80 Returns a boolean representing whether or not the two Specifier-like81 objects are equal.82 83 :param other: The other object to check against.84 """85 86 @property87 @abc.abstractmethod88 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 disable92 prereleases or it can be set to ``None`` (the default) to use default semantics.93 """94 95 @prereleases.setter # noqa: B02796 def prereleases(self, value: bool) -> None:97 """Setter for :attr:`prereleases`.98 99 :param value: The value to set.100 """101 102 @abc.abstractmethod103 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.abstractmethod109 def filter(110 self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None111 ) -> Iterator[UnparsedVersionVar]:112 """113 Takes an iterable of items and filters them so that only items which114 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 instead124 prefer to work with :class:`SpecifierSet` instead, which can parse125 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 will137 # do an exact string match of the version you wish to install.138 # This will not be parsed by PEP 440 and we cannot determine139 # any semantic meaning from it. This operator is discouraged140 # but included entirely as an escape hatch.141 (?<====) # Only match for the identity operator142 \s*143 [^\s;)]* # The arbitrary version can be just about anything,144 # we match everything except for whitespace, a145 # semi-colon for marker support, and a closing paren146 # since versions can be enclosed in them.147 )148 |149 (?:150 # The (non)equality operators allow for wild card and local151 # versions to be specified so we have to define these two152 # operators separately to enable that.153 (?<===|!=) # Only match for equals and not equals154 155 \s*156 v?157 (?:[0-9]+!)? # epoch158 [0-9]+(?:\.[0-9]+)* # release159 160 # You cannot use a wild card and a pre-release, post-release, a dev or161 # local version together so group them with a | and make them optional.162 (?:163 \.\* # Wild card syntax of .*164 |165 (?: # pre release166 [-_\.]?167 (alpha|beta|preview|pre|a|b|c|rc)168 [-_\.]?169 [0-9]*170 )?171 (?: # post release172 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)173 )?174 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release175 (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local176 )?177 )178 |179 (?:180 # The compatible operator requires at least two digits in the181 # release segment.182 (?<=~=) # Only match for the compatible operator183 184 \s*185 v?186 (?:[0-9]+!)? # epoch187 [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *)188 (?: # pre release189 [-_\.]?190 (alpha|beta|preview|pre|a|b|c|rc)191 [-_\.]?192 [0-9]*193 )?194 (?: # post release195 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)196 )?197 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release198 )199 |200 (?:201 # All other operators only allow a sub set of what the202 # (non)equality operators do. Specifically they do not allow203 # local versions to be specified nor do they allow the prefix204 # matching wild cards.205 (?<!==|!=|~=) # We have special cases for these206 # operators so we want to make sure they207 # don't match here.208 209 \s*210 v?211 (?:[0-9]+!)? # epoch212 [0-9]+(?:\.[0-9]+)* # release213 (?: # pre release214 [-_\.]?215 (alpha|beta|preview|pre|a|b|c|rc)216 [-_\.]?217 [0-9]*218 )?219 (?: # post release220 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)221 )?222 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release223 )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 and248 normalized before use.249 :param prereleases:250 This tells the specifier if it should accept prerelease versions if251 applicable or not. The default of ``None`` will autodetect it from the252 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 prereleases266 self._prereleases = prereleases267 268 # Specifier version cache269 self._spec_version: tuple[str, Version] | None = None270 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 None279 280 self._spec_version = (version, version_specifier)281 return version_specifier282 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 version287 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 None291 return spec_version292 293 @property294 def prereleases(self) -> bool | None:295 # If there is an explicit prereleases set for this, then we'll just296 # blindly use that.297 if self._prereleases is not None:298 return self._prereleases299 300 # Only the "!=" operator does not imply prereleases when301 # the version in the specifier is a prerelease.302 operator, version_str = self._spec303 if operator != "!=":304 # The == specifier with trailing .* cannot include prereleases305 # e.g. "==1.0a1.*" is not valid.306 if operator == "==" and version_str.endswith(".*"):307 return False308 309 # "===" can have arbitrary string versions, so we cannot parse310 # those, we take prereleases as unknown (None) for those.311 version = self._get_spec_version(version_str)312 if version is None:313 return None314 315 # For all other operators, use the check if spec Version316 # object implies pre-releases.317 if version.is_prerelease:318 return True319 320 return False321 322 @prereleases.setter323 def prereleases(self, value: bool | None) -> None:324 self._prereleases = value325 326 @property327 def operator(self) -> str:328 """The operator of this specifier.329 330 >>> Specifier("==1.2.3").operator331 '=='332 """333 return self._spec[0]334 335 @property336 def version(self) -> str:337 """The version of this specifier.338 339 >>> Specifier("==1.2.3").version340 '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 None357 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 @property373 def _canonical_spec(self) -> tuple[str, str]:374 operator, version = self._spec375 if operator == "===" or version.endswith(".*"):376 return operator, version377 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_version385 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 True398 >>> (Specifier("==1.2.3", prereleases=False) ==399 ... Specifier("==1.2.3", prereleases=True))400 True401 >>> Specifier("==1.2.3") == "==1.2.3"402 True403 >>> Specifier("==1.2.3") == Specifier("==1.2.4")404 False405 >>> Specifier("==1.2.3") == Specifier("~=1.2.3")406 False407 """408 if isinstance(other, str):409 try:410 other = self.__class__(str(other))411 except InvalidSpecifier:412 return NotImplemented413 elif not isinstance(other, self.__class__):414 return NotImplemented415 416 return self._canonical_spec == other._canonical_spec417 418 def _get_operator(self, op: str) -> CallableOperator:419 operator_callable: CallableOperator = getattr(420 self, f"_compare_{self._operators[op]}"421 )422 return operator_callable423 424 def _compare_compatible(self, prospective: Version, spec: str) -> bool:425 # Compatible releases have an equivalent combination of >= and ==. That426 # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to427 # implement this in terms of the other specifiers instead of428 # implementing it ourselves. The only thing we need to do is construct429 # the other specifiers.430 431 # We want everything but the last item in the version, but we want to432 # 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 string438 prefix += ".*"439 440 return self._get_operator(">=")(prospective, spec) and self._get_operator("==")(441 prospective, prefix442 )443 444 def _compare_equal(self, prospective: Version, spec: str) -> bool:445 # We need special logic to handle prefix matching446 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=False450 )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 is454 # 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 pretend458 # that there is an implicit dot in between a release segment and459 # a pre-release segment.460 split_prospective = _version_split(normalized_prospective)461 462 # 0-pad the prospective version before shortening it to get the correct463 # shortened version.464 padded_prospective, _ = _pad_version(split_prospective, split_spec)465 466 # Shorten the prospective version to be the same length as the spec467 # so that we can determine if the specifier is a prefix of the468 # prospective version or not.469 shortened_prospective = padded_prospective[: len(split_spec)]470 471 return shortened_prospective == split_spec472 else:473 # Convert our spec string into a Version474 spec_version = self._require_spec_version(spec)475 476 # If the specifier does not have a local segment, then we want to477 # act as if the prospective version also does not have a local478 # segment.479 if not spec_version.local:480 prospective = _public_version(prospective)481 482 return prospective == spec_version483 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 version489 # specifier, so local version labels can be universally removed from490 # 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 version495 # specifier, so local version labels can be universally removed from496 # 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 with501 # 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 spec505 # version. If it's not we can short circuit and just return False now506 # instead of doing extra unneeded work.507 if not prospective < spec:508 return False509 510 # This special case is here so that, unless the specifier itself511 # includes is a pre-release version, that we do not accept pre-release512 # versions for the version mentioned in the specifier (e.g. <3.1 should513 # not match 3.1.dev0, but should match 3.0.dev0).514 if (515 not spec.is_prerelease516 and prospective.is_prerelease517 and _base_version(prospective) == _base_version(spec)518 ):519 return False520 521 # If we've gotten to here, it means that prospective version is both522 # less than the spec version *and* it's not a pre-release of the same523 # version in the spec.524 return True525 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 with528 # 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 spec532 # version. If it's not we can short circuit and just return False now533 # instead of doing extra unneeded work.534 if not prospective > spec:535 return False536 537 # This special case is here so that, unless the specifier itself538 # includes is a post-release version, that we do not accept539 # post-release versions for the version mentioned in the specifier540 # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0).541 if (542 not spec.is_postrelease543 and prospective.is_postrelease544 and _base_version(prospective) == _base_version(spec)545 ):546 return False547 548 # Ensure that we do not allow a local version of the version mentioned549 # in the specifier, which is technically greater than, to match.550 if prospective.local is not None and _base_version(551 prospective552 ) == _base_version(spec):553 return False554 555 # If we've gotten to here, it means that prospective version is both556 # greater than the spec version *and* it's not a pre-release of the557 # same version in the spec.558 return True559 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 as569 :meth:`contains` with no ``prereleases`` argument passed.570 571 >>> "1.2.3" in Specifier(">=1.2.3")572 True573 >>> Version("1.2.3") in Specifier(">=1.2.3")574 True575 >>> "1.0.0" in Specifier(">=1.2.3")576 False577 >>> "1.3.0a1" in Specifier(">=1.2.3")578 True579 >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True)580 True581 """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 a589 :class:`Version` instance.590 :param prereleases:591 Whether or not to match prereleases with this Specifier. If set to592 ``None`` (the default), it will follow the recommendation from593 :pep:`440` and match prereleases, as there are no other versions.594 595 >>> Specifier(">=1.2.3").contains("1.2.3")596 True597 >>> Specifier(">=1.2.3").contains(Version("1.2.3"))598 True599 >>> Specifier(">=1.2.3").contains("1.0.0")600 False601 >>> Specifier(">=1.2.3").contains("1.3.0a1")602 True603 >>> Specifier(">=1.2.3", prereleases=False).contains("1.3.0a1")604 False605 >>> Specifier(">=1.2.3").contains("1.3.0a1")606 True607 """608 609 return bool(list(self.filter([item], prereleases=prereleases)))610 611 def filter(612 self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None613 ) -> 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 to621 ``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 = False637 638 # Determine if to include prereleases by default639 include_prereleases = (640 prereleases if prereleases is not None else self.prereleases641 )642 643 # Get the matching operator644 operator_callable = self._get_operator(self.operator)645 646 # Filter versions647 for version in iterable:648 parsed_version = _coerce_version(version)649 if parsed_version is None:650 # === operator can match arbitrary (non-version) strings651 if self.operator == "===" and self._compare_arbitrary(652 version, self.version653 ):654 yield version655 elif operator_callable(parsed_version, self.version):656 # If it's not a prerelease or prereleases are allowed, yield it directly657 if not parsed_version.is_prerelease or include_prereleases:658 found_non_prereleases = True659 yield version660 # Otherwise collect prereleases for potential later use661 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't665 # explicitly forbidden, yield the collected prereleases666 if (667 not found_non_prereleases668 and prereleases is None669 and self._prereleases is not False670 ):671 yield from prereleases_versions672 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 does681 not attempt to retain the original version string, so joining the682 components back with :func:`_version_join` may not produce the original683 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 result697 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 the703 first component must be the epoch (either empty or numeric), and all other704 components numeric.705 """706 epoch, *rest = components707 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 versions720 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 versions724 left_split.append(left[len(left_split[0]) :])725 right_split.append(right[len(right_split[0]) :])726 727 # Insert our padding728 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 of741 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 of755 specifiers which will be parsed and normalized before use.756 May also be an iterable of ``Specifier`` instances, which will be used757 as is.758 :param prereleases:759 This tells the SpecifierSet if it should accept prerelease versions if760 applicable or not. The default of ``None`` will autodetect it from the761 given specifiers.762 763 :raises InvalidSpecifier:764 If the given ``specifiers`` are not parseable than this exception will be765 raised.766 """767 768 if isinstance(specifiers, str):769 # Split on `,` to break each individual specifier into its own item, and770 # 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 set774 # 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 if781 # we accept prereleases or not.782 self._prereleases = prereleases783 784 @property785 def prereleases(self) -> bool | None:786 # If we have been given an explicit prerelease modifier, then we'll787 # pass that through here.788 if self._prereleases is not None:789 return self._prereleases790 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 have793 # pre-releases or not.794 if not self._specs:795 return None796 797 # Otherwise we'll see if any of the given specifiers accept798 # prereleases, if any of them do we'll return True, otherwise False.799 if any(s.prereleases for s in self._specs):800 return True801 802 return None803 804 @prereleases.setter805 def prereleases(self, value: bool | None) -> None:806 self._prereleases = value807 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 not812 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 None824 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 not833 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 NotImplemented859 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._prereleases865 elif (866 self._prereleases is not None and other._prereleases is None867 ) or self._prereleases == other._prereleases:868 specifier._prereleases = self._prereleases869 else:870 raise ValueError(871 "Cannot combine SpecifierSets with True and False prerelease overrides."872 )873 874 return specifier875 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 True885 >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) ==886 ... SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True))887 True888 >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1"889 True890 >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0")891 False892 >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2")893 False894 """895 if isinstance(other, (str, Specifier)):896 other = SpecifierSet(str(other))897 elif not isinstance(other, SpecifierSet):898 return NotImplemented899 900 return self._specs == other._specs901 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` instances909 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 as922 :meth:`contains` with no ``prereleases`` argument passed.923 924 >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1")925 True926 >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1")927 True928 >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1")929 False930 >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1")931 True932 >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)933 True934 """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 a947 :class:`Version` instance.948 :param prereleases:949 Whether or not to match prereleases with this SpecifierSet. If set to950 ``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 will954 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 True958 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3"))959 True960 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1")961 False962 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1")963 True964 >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False).contains("1.3.0a1")965 False966 >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True)967 True968 """969 version = _coerce_version(item)970 971 if version is not None and installed and version.is_prerelease:972 prereleases = True973 974 check_item = item if version is None else version975 return bool(list(self.filter([check_item], prereleases=prereleases)))976 977 def filter(978 self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None979 ) -> 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 to987 ``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 prerelease1002 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 forcing1014 # one for this particular filter call, then we'll use whatever the1015 # SpecifierSet thinks for whether or not we should support prereleases.1016 if prereleases is None and self.prereleases is not None:1017 prereleases = self.prereleases1018 1019 # If we have any specifiers, then we want to wrap our iterable in the1020 # filter method for each one, this will act as a logical AND amongst1021 # each specifier.1022 if self._specs:1023 # When prereleases is None, we need to let all versions through1024 # the individual filters, then decide about prereleases at the end1025 # 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 prereleases1029 )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 item1043 for item in iterable1044 if (version := _coerce_version(item)) is None1045 or not version.is_prerelease1046 )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 = False1053 1054 for item in iterable:1055 parsed_version = _coerce_version(item)1056 # Arbitrary strings are always included as it is not1057 # 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 = True1067 1068 return iter(filtered_items if found_final_release else found_prereleases)1069 