| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068 |
- # 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
- """
- from __future__ import annotations
- import abc
- import itertools
- import re
- from typing import Callable, Final, Iterable, Iterator, TypeVar, Union
- from .utils import canonicalize_version
- from .version import InvalidVersion, Version
- UnparsedVersion = Union[Version, str]
- UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion)
- CallableOperator = Callable[[Version, str], bool]
- def _coerce_version(version: UnparsedVersion) -> Version | None:
- if not isinstance(version, Version):
- try:
- version = Version(version)
- except InvalidVersion:
- return None
- return version
- def _public_version(version: Version) -> Version:
- return version.__replace__(local=None)
- def _base_version(version: Version) -> Version:
- return version.__replace__(pre=None, post=None, dev=None, local=None)
- 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):
- __slots__ = ()
- __match_args__ = ("_str",)
- @property
- def _str(self) -> str:
- """Internal property for match_args"""
- return str(self)
- @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) -> bool | None:
- """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 # noqa: B027
- def prereleases(self, value: bool) -> None:
- """Setter for :attr:`prereleases`.
- :param value: The value to set.
- """
- @abc.abstractmethod
- def contains(self, item: str, prereleases: bool | None = None) -> bool:
- """
- Determines if the given item is contained within this specifier.
- """
- @abc.abstractmethod
- def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = 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).
- """
- __slots__ = ("_prereleases", "_spec", "_spec_version")
- _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: Final = {
- "~=": "compatible",
- "==": "equal",
- "!=": "not_equal",
- "<=": "less_than_equal",
- ">=": "greater_than_equal",
- "<": "less_than",
- ">": "greater_than",
- "===": "arbitrary",
- }
- def __init__(self, spec: str = "", prereleases: bool | None = 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.fullmatch(spec)
- if not match:
- raise InvalidSpecifier(f"Invalid specifier: {spec!r}")
- 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
- # Specifier version cache
- self._spec_version: tuple[str, Version] | None = None
- def _get_spec_version(self, version: str) -> Version | None:
- """One element cache, as only one spec Version is needed per Specifier."""
- if self._spec_version is not None and self._spec_version[0] == version:
- return self._spec_version[1]
- version_specifier = _coerce_version(version)
- if version_specifier is None:
- return None
- self._spec_version = (version, version_specifier)
- return version_specifier
- def _require_spec_version(self, version: str) -> Version:
- """Get spec version, asserting it's valid (not for === operator).
- This method should only be called for operators where version
- strings are guaranteed to be valid PEP 440 versions (not ===).
- """
- spec_version = self._get_spec_version(version)
- assert spec_version is not None
- return spec_version
- @property
- def prereleases(self) -> bool | None:
- # 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
- # Only the "!=" operator does not imply prereleases when
- # the version in the specifier is a prerelease.
- operator, version_str = self._spec
- if operator != "!=":
- # The == specifier with trailing .* cannot include prereleases
- # e.g. "==1.0a1.*" is not valid.
- if operator == "==" and version_str.endswith(".*"):
- return False
- # "===" can have arbitrary string versions, so we cannot parse
- # those, we take prereleases as unknown (None) for those.
- version = self._get_spec_version(version_str)
- if version is None:
- return None
- # For all other operators, use the check if spec Version
- # object implies pre-releases.
- if version.is_prerelease:
- return True
- return False
- @prereleases.setter
- def prereleases(self, value: bool | None) -> 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]:
- operator, version = self._spec
- if operator == "===" or version.endswith(".*"):
- return operator, version
- spec_version = self._require_spec_version(version)
- canonical_version = canonicalize_version(
- spec_version, strip_trailing_zero=(operator != "~=")
- )
- return operator, 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 = _version_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(
- _public_version(prospective), 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 bangs and 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 bangs and 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 = self._require_spec_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 = _public_version(prospective)
- 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 _public_version(prospective) <= self._require_spec_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 _public_version(prospective) >= self._require_spec_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 = self._require_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
- and _base_version(prospective) == _base_version(spec)
- ):
- 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 = self._require_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
- and _base_version(prospective) == _base_version(spec)
- ):
- 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 and _base_version(
- prospective
- ) == _base_version(spec):
- 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 | str, spec: str) -> bool:
- return str(prospective).lower() == str(spec).lower()
- def __contains__(self, item: 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")
- True
- >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True)
- True
- """
- return self.contains(item)
- def contains(self, item: UnparsedVersion, prereleases: bool | None = 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 will follow the recommendation from
- :pep:`440` and match prereleases, as there are no other versions.
- >>> 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")
- True
- >>> Specifier(">=1.2.3", prereleases=False).contains("1.3.0a1")
- False
- >>> Specifier(">=1.2.3").contains("1.3.0a1")
- True
- """
- return bool(list(self.filter([item], prereleases=prereleases)))
- def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = 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 follow the recommendation from :pep:`440`
- and match prereleases if there are no other versions.
- >>> 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']
- """
- prereleases_versions = []
- found_non_prereleases = False
- # Determine if to include prereleases by default
- include_prereleases = (
- prereleases if prereleases is not None else self.prereleases
- )
- # Get the matching operator
- operator_callable = self._get_operator(self.operator)
- # Filter versions
- for version in iterable:
- parsed_version = _coerce_version(version)
- if parsed_version is None:
- # === operator can match arbitrary (non-version) strings
- if self.operator == "===" and self._compare_arbitrary(
- version, self.version
- ):
- yield version
- elif operator_callable(parsed_version, self.version):
- # If it's not a prerelease or prereleases are allowed, yield it directly
- if not parsed_version.is_prerelease or include_prereleases:
- found_non_prereleases = True
- yield version
- # Otherwise collect prereleases for potential later use
- elif prereleases is None and self._prereleases is not False:
- prereleases_versions.append(version)
- # If no non-prereleases were found and prereleases weren't
- # explicitly forbidden, yield the collected prereleases
- if (
- not found_non_prereleases
- and prereleases is None
- and self._prereleases is not False
- ):
- yield from prereleases_versions
- _prefix_regex = re.compile(r"([0-9]+)((?:a|b|c|rc)[0-9]+)")
- def _version_split(version: str) -> list[str]:
- """Split version into components.
- The split components are intended for version comparison. The logic does
- not attempt to retain the original version string, so joining the
- components back with :func:`_version_join` may not produce the original
- version string.
- """
- result: list[str] = []
- epoch, _, rest = version.rpartition("!")
- result.append(epoch or "0")
- for item in rest.split("."):
- match = _prefix_regex.fullmatch(item)
- if match:
- result.extend(match.groups())
- else:
- result.append(item)
- return result
- def _version_join(components: list[str]) -> str:
- """Join split version components into a version string.
- This function assumes the input came from :func:`_version_split`, where the
- first component must be the epoch (either empty or numeric), and all other
- components numeric.
- """
- epoch, *rest = components
- return f"{epoch}!{'.'.join(rest)}"
- 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.from_iterable(left_split)),
- list(itertools.chain.from_iterable(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.
- """
- __slots__ = ("_prereleases", "_specs")
- def __init__(
- self,
- specifiers: str | Iterable[Specifier] = "",
- prereleases: bool | None = 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.
- May also be an iterable of ``Specifier`` instances, which will be used
- as is.
- :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.
- """
- if isinstance(specifiers, str):
- # Split on `,` to break each individual specifier into its own item, and
- # strip each item to remove leading/trailing whitespace.
- split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
- # Make each individual specifier a Specifier and save in a frozen set
- # for later.
- self._specs = frozenset(map(Specifier, split_specifiers))
- else:
- # Save the supplied specifiers in a frozen set.
- self._specs = frozenset(specifiers)
- # 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) -> bool | None:
- # 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.
- if any(s.prereleases for s in self._specs):
- return True
- return None
- @prereleases.setter
- def prereleases(self, value: bool | None) -> 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: 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
- ) or 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")
- True
- >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)
- True
- """
- return self.contains(item)
- def contains(
- self,
- item: UnparsedVersion,
- prereleases: bool | None = None,
- installed: bool | None = 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 will follow the recommendation from :pep:`440`
- and match prereleases, as there are no other versions.
- :param installed:
- Whether or not the item is installed. If set to ``True``, it will
- accept prerelease versions even if the specifier does not allow them.
- >>> 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")
- True
- >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False).contains("1.3.0a1")
- False
- >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True)
- True
- """
- version = _coerce_version(item)
- if version is not None and installed and version.is_prerelease:
- prereleases = True
- check_item = item if version is None else version
- return bool(list(self.filter([check_item], prereleases=prereleases)))
- def filter(
- self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = 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 follow the recommendation from :pep:`440`
- and match prereleases if there are no other versions.
- >>> 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"]))
- ['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 and self.prereleases is not 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:
- # When prereleases is None, we need to let all versions through
- # the individual filters, then decide about prereleases at the end
- # based on whether any non-prereleases matched ALL specs.
- for spec in self._specs:
- iterable = spec.filter(
- iterable, prereleases=True if prereleases is None else prereleases
- )
- if prereleases is not None:
- # If we have a forced prereleases value,
- # we can immediately return the iterator.
- return iter(iterable)
- else:
- # Handle empty SpecifierSet cases where prereleases is not None.
- if prereleases is True:
- return iter(iterable)
- if prereleases is False:
- return (
- item
- for item in iterable
- if (version := _coerce_version(item)) is None
- or not version.is_prerelease
- )
- # Finally if prereleases is None, apply PEP 440 logic:
- # exclude prereleases unless there are no final releases that matched.
- filtered_items: list[UnparsedVersionVar] = []
- found_prereleases: list[UnparsedVersionVar] = []
- found_final_release = False
- for item in iterable:
- parsed_version = _coerce_version(item)
- # Arbitrary strings are always included as it is not
- # possible to determine if they are prereleases,
- # and they have already passed all specifiers.
- if parsed_version is None:
- filtered_items.append(item)
- found_prereleases.append(item)
- elif parsed_version.is_prerelease:
- found_prereleases.append(item)
- else:
- filtered_items.append(item)
- found_final_release = True
- return iter(filtered_items if found_final_release else found_prereleases)
|