|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008 |
- # This file is dual licensed under the terms of the Apache License, Version
- # 2.0, and the BSD License. See the LICENSE file in the root of this repository
- # for complete details.
- """
- .. testsetup::
-
- from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier
- from packaging.version import Version
- """
-
- import abc
- import itertools
- import re
- from typing import (
- Callable,
- Iterable,
- Iterator,
- List,
- Optional,
- Set,
- Tuple,
- TypeVar,
- Union,
- )
-
- from .utils import canonicalize_version
- from .version import Version
-
- UnparsedVersion = Union[Version, str]
- UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion)
- CallableOperator = Callable[[Version, str], bool]
-
-
- def _coerce_version(version: UnparsedVersion) -> Version:
- if not isinstance(version, Version):
- version = Version(version)
- return version
-
-
- class InvalidSpecifier(ValueError):
- """
- Raised when attempting to create a :class:`Specifier` with a specifier
- string that is invalid.
-
- >>> Specifier("lolwat")
- Traceback (most recent call last):
- ...
- packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat'
- """
-
-
- class BaseSpecifier(metaclass=abc.ABCMeta):
- @abc.abstractmethod
- def __str__(self) -> str:
- """
- Returns the str representation of this Specifier-like object. This
- should be representative of the Specifier itself.
- """
-
- @abc.abstractmethod
- def __hash__(self) -> int:
- """
- Returns a hash value for this Specifier-like object.
- """
-
- @abc.abstractmethod
- def __eq__(self, other: object) -> bool:
- """
- Returns a boolean representing whether or not the two Specifier-like
- objects are equal.
-
- :param other: The other object to check against.
- """
-
- @property
- @abc.abstractmethod
- def prereleases(self) -> Optional[bool]:
- """Whether or not pre-releases as a whole are allowed.
-
- This can be set to either ``True`` or ``False`` to explicitly enable or disable
- prereleases or it can be set to ``None`` (the default) to use default semantics.
- """
-
- @prereleases.setter
- def prereleases(self, value: bool) -> None:
- """Setter for :attr:`prereleases`.
-
- :param value: The value to set.
- """
-
- @abc.abstractmethod
- def contains(self, item: str, prereleases: Optional[bool] = None) -> bool:
- """
- Determines if the given item is contained within this specifier.
- """
-
- @abc.abstractmethod
- def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
- ) -> Iterator[UnparsedVersionVar]:
- """
- Takes an iterable of items and filters them so that only items which
- are contained within this specifier are allowed in it.
- """
-
-
- class Specifier(BaseSpecifier):
- """This class abstracts handling of version specifiers.
-
- .. tip::
-
- It is generally not required to instantiate this manually. You should instead
- prefer to work with :class:`SpecifierSet` instead, which can parse
- comma-separated version specifiers (which is what package metadata contains).
- """
-
- _operator_regex_str = r"""
- (?P<operator>(~=|==|!=|<=|>=|<|>|===))
- """
- _version_regex_str = r"""
- (?P<version>
- (?:
- # The identity operators allow for an escape hatch that will
- # do an exact string match of the version you wish to install.
- # This will not be parsed by PEP 440 and we cannot determine
- # any semantic meaning from it. This operator is discouraged
- # but included entirely as an escape hatch.
- (?<====) # Only match for the identity operator
- \s*
- [^\s;)]* # The arbitrary version can be just about anything,
- # we match everything except for whitespace, a
- # semi-colon for marker support, and a closing paren
- # since versions can be enclosed in them.
- )
- |
- (?:
- # The (non)equality operators allow for wild card and local
- # versions to be specified so we have to define these two
- # operators separately to enable that.
- (?<===|!=) # Only match for equals and not equals
-
- \s*
- v?
- (?:[0-9]+!)? # epoch
- [0-9]+(?:\.[0-9]+)* # release
-
- # You cannot use a wild card and a pre-release, post-release, a dev or
- # local version together so group them with a | and make them optional.
- (?:
- \.\* # Wild card syntax of .*
- |
- (?: # pre release
- [-_\.]?
- (alpha|beta|preview|pre|a|b|c|rc)
- [-_\.]?
- [0-9]*
- )?
- (?: # post release
- (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
- )?
- (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
- (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
- )?
- )
- |
- (?:
- # The compatible operator requires at least two digits in the
- # release segment.
- (?<=~=) # Only match for the compatible operator
-
- \s*
- v?
- (?:[0-9]+!)? # epoch
- [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *)
- (?: # pre release
- [-_\.]?
- (alpha|beta|preview|pre|a|b|c|rc)
- [-_\.]?
- [0-9]*
- )?
- (?: # post release
- (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
- )?
- (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
- )
- |
- (?:
- # All other operators only allow a sub set of what the
- # (non)equality operators do. Specifically they do not allow
- # local versions to be specified nor do they allow the prefix
- # matching wild cards.
- (?<!==|!=|~=) # We have special cases for these
- # operators so we want to make sure they
- # don't match here.
-
- \s*
- v?
- (?:[0-9]+!)? # epoch
- [0-9]+(?:\.[0-9]+)* # release
- (?: # pre release
- [-_\.]?
- (alpha|beta|preview|pre|a|b|c|rc)
- [-_\.]?
- [0-9]*
- )?
- (?: # post release
- (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
- )?
- (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
- )
- )
- """
-
- _regex = re.compile(
- r"^\s*" + _operator_regex_str + _version_regex_str + r"\s*$",
- re.VERBOSE | re.IGNORECASE,
- )
-
- _operators = {
- "~=": "compatible",
- "==": "equal",
- "!=": "not_equal",
- "<=": "less_than_equal",
- ">=": "greater_than_equal",
- "<": "less_than",
- ">": "greater_than",
- "===": "arbitrary",
- }
-
- def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
- """Initialize a Specifier instance.
-
- :param spec:
- The string representation of a specifier which will be parsed and
- normalized before use.
- :param prereleases:
- This tells the specifier if it should accept prerelease versions if
- applicable or not. The default of ``None`` will autodetect it from the
- given specifiers.
- :raises InvalidSpecifier:
- If the given specifier is invalid (i.e. bad syntax).
- """
- match = self._regex.search(spec)
- if not match:
- raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
-
- self._spec: Tuple[str, str] = (
- match.group("operator").strip(),
- match.group("version").strip(),
- )
-
- # Store whether or not this Specifier should accept prereleases
- self._prereleases = prereleases
-
- # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515
- @property # type: ignore[override]
- def prereleases(self) -> bool:
- # If there is an explicit prereleases set for this, then we'll just
- # blindly use that.
- if self._prereleases is not None:
- return self._prereleases
-
- # Look at all of our specifiers and determine if they are inclusive
- # operators, and if they are if they are including an explicit
- # prerelease.
- operator, version = self._spec
- if operator in ["==", ">=", "<=", "~=", "==="]:
- # The == specifier can include a trailing .*, if it does we
- # want to remove before parsing.
- if operator == "==" and version.endswith(".*"):
- version = version[:-2]
-
- # Parse the version, and if it is a pre-release than this
- # specifier allows pre-releases.
- if Version(version).is_prerelease:
- return True
-
- return False
-
- @prereleases.setter
- def prereleases(self, value: bool) -> None:
- self._prereleases = value
-
- @property
- def operator(self) -> str:
- """The operator of this specifier.
-
- >>> Specifier("==1.2.3").operator
- '=='
- """
- return self._spec[0]
-
- @property
- def version(self) -> str:
- """The version of this specifier.
-
- >>> Specifier("==1.2.3").version
- '1.2.3'
- """
- return self._spec[1]
-
- def __repr__(self) -> str:
- """A representation of the Specifier that shows all internal state.
-
- >>> Specifier('>=1.0.0')
- <Specifier('>=1.0.0')>
- >>> Specifier('>=1.0.0', prereleases=False)
- <Specifier('>=1.0.0', prereleases=False)>
- >>> Specifier('>=1.0.0', prereleases=True)
- <Specifier('>=1.0.0', prereleases=True)>
- """
- pre = (
- f", prereleases={self.prereleases!r}"
- if self._prereleases is not None
- else ""
- )
-
- return f"<{self.__class__.__name__}({str(self)!r}{pre})>"
-
- def __str__(self) -> str:
- """A string representation of the Specifier that can be round-tripped.
-
- >>> str(Specifier('>=1.0.0'))
- '>=1.0.0'
- >>> str(Specifier('>=1.0.0', prereleases=False))
- '>=1.0.0'
- """
- return "{}{}".format(*self._spec)
-
- @property
- def _canonical_spec(self) -> Tuple[str, str]:
- canonical_version = canonicalize_version(
- self._spec[1],
- strip_trailing_zero=(self._spec[0] != "~="),
- )
- return self._spec[0], canonical_version
-
- def __hash__(self) -> int:
- return hash(self._canonical_spec)
-
- def __eq__(self, other: object) -> bool:
- """Whether or not the two Specifier-like objects are equal.
-
- :param other: The other object to check against.
-
- The value of :attr:`prereleases` is ignored.
-
- >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0")
- True
- >>> (Specifier("==1.2.3", prereleases=False) ==
- ... Specifier("==1.2.3", prereleases=True))
- True
- >>> Specifier("==1.2.3") == "==1.2.3"
- True
- >>> Specifier("==1.2.3") == Specifier("==1.2.4")
- False
- >>> Specifier("==1.2.3") == Specifier("~=1.2.3")
- False
- """
- if isinstance(other, str):
- try:
- other = self.__class__(str(other))
- except InvalidSpecifier:
- return NotImplemented
- elif not isinstance(other, self.__class__):
- return NotImplemented
-
- return self._canonical_spec == other._canonical_spec
-
- def _get_operator(self, op: str) -> CallableOperator:
- operator_callable: CallableOperator = getattr(
- self, f"_compare_{self._operators[op]}"
- )
- return operator_callable
-
- def _compare_compatible(self, prospective: Version, spec: str) -> bool:
-
- # Compatible releases have an equivalent combination of >= and ==. That
- # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
- # implement this in terms of the other specifiers instead of
- # implementing it ourselves. The only thing we need to do is construct
- # the other specifiers.
-
- # We want everything but the last item in the version, but we want to
- # ignore suffix segments.
- prefix = ".".join(
- list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1]
- )
-
- # Add the prefix notation to the end of our string
- prefix += ".*"
-
- return self._get_operator(">=")(prospective, spec) and self._get_operator("==")(
- prospective, prefix
- )
-
- def _compare_equal(self, prospective: Version, spec: str) -> bool:
-
- # We need special logic to handle prefix matching
- if spec.endswith(".*"):
- # In the case of prefix matching we want to ignore local segment.
- normalized_prospective = canonicalize_version(
- prospective.public, strip_trailing_zero=False
- )
- # Get the normalized version string ignoring the trailing .*
- normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
- # Split the spec out by dots, and pretend that there is an implicit
- # dot in between a release segment and a pre-release segment.
- split_spec = _version_split(normalized_spec)
-
- # Split the prospective version out by dots, and pretend that there
- # is an implicit dot in between a release segment and a pre-release
- # segment.
- split_prospective = _version_split(normalized_prospective)
-
- # 0-pad the prospective version before shortening it to get the correct
- # shortened version.
- padded_prospective, _ = _pad_version(split_prospective, split_spec)
-
- # Shorten the prospective version to be the same length as the spec
- # so that we can determine if the specifier is a prefix of the
- # prospective version or not.
- shortened_prospective = padded_prospective[: len(split_spec)]
-
- return shortened_prospective == split_spec
- else:
- # Convert our spec string into a Version
- spec_version = Version(spec)
-
- # If the specifier does not have a local segment, then we want to
- # act as if the prospective version also does not have a local
- # segment.
- if not spec_version.local:
- prospective = Version(prospective.public)
-
- return prospective == spec_version
-
- def _compare_not_equal(self, prospective: Version, spec: str) -> bool:
- return not self._compare_equal(prospective, spec)
-
- def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
-
- # NB: Local version identifiers are NOT permitted in the version
- # specifier, so local version labels can be universally removed from
- # the prospective version.
- return Version(prospective.public) <= Version(spec)
-
- def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
-
- # NB: Local version identifiers are NOT permitted in the version
- # specifier, so local version labels can be universally removed from
- # the prospective version.
- return Version(prospective.public) >= Version(spec)
-
- def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
-
- # Convert our spec to a Version instance, since we'll want to work with
- # it as a version.
- spec = Version(spec_str)
-
- # Check to see if the prospective version is less than the spec
- # version. If it's not we can short circuit and just return False now
- # instead of doing extra unneeded work.
- if not prospective < spec:
- return False
-
- # This special case is here so that, unless the specifier itself
- # includes is a pre-release version, that we do not accept pre-release
- # versions for the version mentioned in the specifier (e.g. <3.1 should
- # not match 3.1.dev0, but should match 3.0.dev0).
- if not spec.is_prerelease and prospective.is_prerelease:
- if Version(prospective.base_version) == Version(spec.base_version):
- return False
-
- # If we've gotten to here, it means that prospective version is both
- # less than the spec version *and* it's not a pre-release of the same
- # version in the spec.
- return True
-
- def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
-
- # Convert our spec to a Version instance, since we'll want to work with
- # it as a version.
- spec = Version(spec_str)
-
- # Check to see if the prospective version is greater than the spec
- # version. If it's not we can short circuit and just return False now
- # instead of doing extra unneeded work.
- if not prospective > spec:
- return False
-
- # This special case is here so that, unless the specifier itself
- # includes is a post-release version, that we do not accept
- # post-release versions for the version mentioned in the specifier
- # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0).
- if not spec.is_postrelease and prospective.is_postrelease:
- if Version(prospective.base_version) == Version(spec.base_version):
- return False
-
- # Ensure that we do not allow a local version of the version mentioned
- # in the specifier, which is technically greater than, to match.
- if prospective.local is not None:
- if Version(prospective.base_version) == Version(spec.base_version):
- return False
-
- # If we've gotten to here, it means that prospective version is both
- # greater than the spec version *and* it's not a pre-release of the
- # same version in the spec.
- return True
-
- def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
- return str(prospective).lower() == str(spec).lower()
-
- def __contains__(self, item: Union[str, Version]) -> bool:
- """Return whether or not the item is contained in this specifier.
-
- :param item: The item to check for.
-
- This is used for the ``in`` operator and behaves the same as
- :meth:`contains` with no ``prereleases`` argument passed.
-
- >>> "1.2.3" in Specifier(">=1.2.3")
- True
- >>> Version("1.2.3") in Specifier(">=1.2.3")
- True
- >>> "1.0.0" in Specifier(">=1.2.3")
- False
- >>> "1.3.0a1" in Specifier(">=1.2.3")
- False
- >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True)
- True
- """
- return self.contains(item)
-
- def contains(
- self, item: UnparsedVersion, prereleases: Optional[bool] = None
- ) -> bool:
- """Return whether or not the item is contained in this specifier.
-
- :param item:
- The item to check for, which can be a version string or a
- :class:`Version` instance.
- :param prereleases:
- Whether or not to match prereleases with this Specifier. If set to
- ``None`` (the default), it uses :attr:`prereleases` to determine
- whether or not prereleases are allowed.
-
- >>> Specifier(">=1.2.3").contains("1.2.3")
- True
- >>> Specifier(">=1.2.3").contains(Version("1.2.3"))
- True
- >>> Specifier(">=1.2.3").contains("1.0.0")
- False
- >>> Specifier(">=1.2.3").contains("1.3.0a1")
- False
- >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1")
- True
- >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True)
- True
- """
-
- # Determine if prereleases are to be allowed or not.
- if prereleases is None:
- prereleases = self.prereleases
-
- # Normalize item to a Version, this allows us to have a shortcut for
- # "2.0" in Specifier(">=2")
- normalized_item = _coerce_version(item)
-
- # Determine if we should be supporting prereleases in this specifier
- # or not, if we do not support prereleases than we can short circuit
- # logic if this version is a prereleases.
- if normalized_item.is_prerelease and not prereleases:
- return False
-
- # Actually do the comparison to determine if this item is contained
- # within this Specifier or not.
- operator_callable: CallableOperator = self._get_operator(self.operator)
- return operator_callable(normalized_item, self.version)
-
- def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
- ) -> Iterator[UnparsedVersionVar]:
- """Filter items in the given iterable, that match the specifier.
-
- :param iterable:
- An iterable that can contain version strings and :class:`Version` instances.
- The items in the iterable will be filtered according to the specifier.
- :param prereleases:
- Whether or not to allow prereleases in the returned iterator. If set to
- ``None`` (the default), it will be intelligently decide whether to allow
- prereleases or not (based on the :attr:`prereleases` attribute, and
- whether the only versions matching are prereleases).
-
- This method is smarter than just ``filter(Specifier().contains, [...])``
- because it implements the rule from :pep:`440` that a prerelease item
- SHOULD be accepted if no other versions match the given specifier.
-
- >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"]))
- ['1.3']
- >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")]))
- ['1.2.3', '1.3', <Version('1.4')>]
- >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"]))
- ['1.5a1']
- >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True))
- ['1.3', '1.5a1']
- >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"]))
- ['1.3', '1.5a1']
- """
-
- yielded = False
- found_prereleases = []
-
- kw = {"prereleases": prereleases if prereleases is not None else True}
-
- # Attempt to iterate over all the values in the iterable and if any of
- # them match, yield them.
- for version in iterable:
- parsed_version = _coerce_version(version)
-
- if self.contains(parsed_version, **kw):
- # If our version is a prerelease, and we were not set to allow
- # prereleases, then we'll store it for later in case nothing
- # else matches this specifier.
- if parsed_version.is_prerelease and not (
- prereleases or self.prereleases
- ):
- found_prereleases.append(version)
- # Either this is not a prerelease, or we should have been
- # accepting prereleases from the beginning.
- else:
- yielded = True
- yield version
-
- # Now that we've iterated over everything, determine if we've yielded
- # any values, and if we have not and we have any prereleases stored up
- # then we will go ahead and yield the prereleases.
- if not yielded and found_prereleases:
- for version in found_prereleases:
- yield version
-
-
- _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
-
-
- def _version_split(version: str) -> List[str]:
- result: List[str] = []
- for item in version.split("."):
- match = _prefix_regex.search(item)
- if match:
- result.extend(match.groups())
- else:
- result.append(item)
- return result
-
-
- def _is_not_suffix(segment: str) -> bool:
- return not any(
- segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
- )
-
-
- def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]:
- left_split, right_split = [], []
-
- # Get the release segment of our versions
- left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
- right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
-
- # Get the rest of our versions
- left_split.append(left[len(left_split[0]) :])
- right_split.append(right[len(right_split[0]) :])
-
- # Insert our padding
- left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0])))
- right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0])))
-
- return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split)))
-
-
- class SpecifierSet(BaseSpecifier):
- """This class abstracts handling of a set of version specifiers.
-
- It can be passed a single specifier (``>=3.0``), a comma-separated list of
- specifiers (``>=3.0,!=3.1``), or no specifier at all.
- """
-
- def __init__(
- self, specifiers: str = "", prereleases: Optional[bool] = None
- ) -> None:
- """Initialize a SpecifierSet instance.
-
- :param specifiers:
- The string representation of a specifier or a comma-separated list of
- specifiers which will be parsed and normalized before use.
- :param prereleases:
- This tells the SpecifierSet if it should accept prerelease versions if
- applicable or not. The default of ``None`` will autodetect it from the
- given specifiers.
-
- :raises InvalidSpecifier:
- If the given ``specifiers`` are not parseable than this exception will be
- raised.
- """
-
- # Split on `,` to break each individual specifier into it's own item, and
- # strip each item to remove leading/trailing whitespace.
- split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
-
- # Parsed each individual specifier, attempting first to make it a
- # Specifier.
- parsed: Set[Specifier] = set()
- for specifier in split_specifiers:
- parsed.add(Specifier(specifier))
-
- # Turn our parsed specifiers into a frozen set and save them for later.
- self._specs = frozenset(parsed)
-
- # Store our prereleases value so we can use it later to determine if
- # we accept prereleases or not.
- self._prereleases = prereleases
-
- @property
- def prereleases(self) -> Optional[bool]:
- # If we have been given an explicit prerelease modifier, then we'll
- # pass that through here.
- if self._prereleases is not None:
- return self._prereleases
-
- # If we don't have any specifiers, and we don't have a forced value,
- # then we'll just return None since we don't know if this should have
- # pre-releases or not.
- if not self._specs:
- return None
-
- # Otherwise we'll see if any of the given specifiers accept
- # prereleases, if any of them do we'll return True, otherwise False.
- return any(s.prereleases for s in self._specs)
-
- @prereleases.setter
- def prereleases(self, value: bool) -> None:
- self._prereleases = value
-
- def __repr__(self) -> str:
- """A representation of the specifier set that shows all internal state.
-
- Note that the ordering of the individual specifiers within the set may not
- match the input string.
-
- >>> SpecifierSet('>=1.0.0,!=2.0.0')
- <SpecifierSet('!=2.0.0,>=1.0.0')>
- >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False)
- <SpecifierSet('!=2.0.0,>=1.0.0', prereleases=False)>
- >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True)
- <SpecifierSet('!=2.0.0,>=1.0.0', prereleases=True)>
- """
- pre = (
- f", prereleases={self.prereleases!r}"
- if self._prereleases is not None
- else ""
- )
-
- return f"<SpecifierSet({str(self)!r}{pre})>"
-
- def __str__(self) -> str:
- """A string representation of the specifier set that can be round-tripped.
-
- Note that the ordering of the individual specifiers within the set may not
- match the input string.
-
- >>> str(SpecifierSet(">=1.0.0,!=1.0.1"))
- '!=1.0.1,>=1.0.0'
- >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False))
- '!=1.0.1,>=1.0.0'
- """
- return ",".join(sorted(str(s) for s in self._specs))
-
- def __hash__(self) -> int:
- return hash(self._specs)
-
- def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet":
- """Return a SpecifierSet which is a combination of the two sets.
-
- :param other: The other object to combine with.
-
- >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1'
- <SpecifierSet('!=1.0.1,!=2.0.1,<=2.0.0,>=1.0.0')>
- >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1')
- <SpecifierSet('!=1.0.1,!=2.0.1,<=2.0.0,>=1.0.0')>
- """
- if isinstance(other, str):
- other = SpecifierSet(other)
- elif not isinstance(other, SpecifierSet):
- return NotImplemented
-
- specifier = SpecifierSet()
- specifier._specs = frozenset(self._specs | other._specs)
-
- if self._prereleases is None and other._prereleases is not None:
- specifier._prereleases = other._prereleases
- elif self._prereleases is not None and other._prereleases is None:
- specifier._prereleases = self._prereleases
- elif self._prereleases == other._prereleases:
- specifier._prereleases = self._prereleases
- else:
- raise ValueError(
- "Cannot combine SpecifierSets with True and False prerelease "
- "overrides."
- )
-
- return specifier
-
- def __eq__(self, other: object) -> bool:
- """Whether or not the two SpecifierSet-like objects are equal.
-
- :param other: The other object to check against.
-
- The value of :attr:`prereleases` is ignored.
-
- >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1")
- True
- >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) ==
- ... SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True))
- True
- >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1"
- True
- >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0")
- False
- >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2")
- False
- """
- if isinstance(other, (str, Specifier)):
- other = SpecifierSet(str(other))
- elif not isinstance(other, SpecifierSet):
- return NotImplemented
-
- return self._specs == other._specs
-
- def __len__(self) -> int:
- """Returns the number of specifiers in this specifier set."""
- return len(self._specs)
-
- def __iter__(self) -> Iterator[Specifier]:
- """
- Returns an iterator over all the underlying :class:`Specifier` instances
- in this specifier set.
-
- >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str)
- [<Specifier('!=1.0.1')>, <Specifier('>=1.0.0')>]
- """
- return iter(self._specs)
-
- def __contains__(self, item: UnparsedVersion) -> bool:
- """Return whether or not the item is contained in this specifier.
-
- :param item: The item to check for.
-
- This is used for the ``in`` operator and behaves the same as
- :meth:`contains` with no ``prereleases`` argument passed.
-
- >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1")
- True
- >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1")
- True
- >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1")
- False
- >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1")
- False
- >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)
- True
- """
- return self.contains(item)
-
- def contains(
- self,
- item: UnparsedVersion,
- prereleases: Optional[bool] = None,
- installed: Optional[bool] = None,
- ) -> bool:
- """Return whether or not the item is contained in this SpecifierSet.
-
- :param item:
- The item to check for, which can be a version string or a
- :class:`Version` instance.
- :param prereleases:
- Whether or not to match prereleases with this SpecifierSet. If set to
- ``None`` (the default), it uses :attr:`prereleases` to determine
- whether or not prereleases are allowed.
-
- >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3")
- True
- >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3"))
- True
- >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1")
- False
- >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1")
- False
- >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1")
- True
- >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True)
- True
- """
- # Ensure that our item is a Version instance.
- if not isinstance(item, Version):
- item = Version(item)
-
- # Determine if we're forcing a prerelease or not, if we're not forcing
- # one for this particular filter call, then we'll use whatever the
- # SpecifierSet thinks for whether or not we should support prereleases.
- if prereleases is None:
- prereleases = self.prereleases
-
- # We can determine if we're going to allow pre-releases by looking to
- # see if any of the underlying items supports them. If none of them do
- # and this item is a pre-release then we do not allow it and we can
- # short circuit that here.
- # Note: This means that 1.0.dev1 would not be contained in something
- # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0
- if not prereleases and item.is_prerelease:
- return False
-
- if installed and item.is_prerelease:
- item = Version(item.base_version)
-
- # We simply dispatch to the underlying specs here to make sure that the
- # given version is contained within all of them.
- # Note: This use of all() here means that an empty set of specifiers
- # will always return True, this is an explicit design decision.
- return all(s.contains(item, prereleases=prereleases) for s in self._specs)
-
- def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
- ) -> Iterator[UnparsedVersionVar]:
- """Filter items in the given iterable, that match the specifiers in this set.
-
- :param iterable:
- An iterable that can contain version strings and :class:`Version` instances.
- The items in the iterable will be filtered according to the specifier.
- :param prereleases:
- Whether or not to allow prereleases in the returned iterator. If set to
- ``None`` (the default), it will be intelligently decide whether to allow
- prereleases or not (based on the :attr:`prereleases` attribute, and
- whether the only versions matching are prereleases).
-
- This method is smarter than just ``filter(SpecifierSet(...).contains, [...])``
- because it implements the rule from :pep:`440` that a prerelease item
- SHOULD be accepted if no other versions match the given specifier.
-
- >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"]))
- ['1.3']
- >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")]))
- ['1.3', <Version('1.4')>]
- >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"]))
- []
- >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True))
- ['1.3', '1.5a1']
- >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"]))
- ['1.3', '1.5a1']
-
- An "empty" SpecifierSet will filter items based on the presence of prerelease
- versions in the set.
-
- >>> list(SpecifierSet("").filter(["1.3", "1.5a1"]))
- ['1.3']
- >>> list(SpecifierSet("").filter(["1.5a1"]))
- ['1.5a1']
- >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"]))
- ['1.3', '1.5a1']
- >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True))
- ['1.3', '1.5a1']
- """
- # Determine if we're forcing a prerelease or not, if we're not forcing
- # one for this particular filter call, then we'll use whatever the
- # SpecifierSet thinks for whether or not we should support prereleases.
- if prereleases is None:
- prereleases = self.prereleases
-
- # If we have any specifiers, then we want to wrap our iterable in the
- # filter method for each one, this will act as a logical AND amongst
- # each specifier.
- if self._specs:
- for spec in self._specs:
- iterable = spec.filter(iterable, prereleases=bool(prereleases))
- return iter(iterable)
- # If we do not have any specifiers, then we need to have a rough filter
- # which will filter out any pre-releases, unless there are no final
- # releases.
- else:
- filtered: List[UnparsedVersionVar] = []
- found_prereleases: List[UnparsedVersionVar] = []
-
- for item in iterable:
- parsed_version = _coerce_version(item)
-
- # Store any item which is a pre-release for later unless we've
- # already found a final version or we are accepting prereleases
- if parsed_version.is_prerelease and not prereleases:
- if not filtered:
- found_prereleases.append(item)
- else:
- filtered.append(item)
-
- # If we've found no items except for pre-releases, then we'll go
- # ahead and use the pre-releases
- if not filtered and found_prereleases and prereleases is None:
- return iter(found_prereleases)
-
- return iter(filtered)
|