| |
| |
| |
| """ |
| .. testsetup:: |
| |
| from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier |
| from packaging.version import Version |
| """ |
|
|
| from __future__ import annotations |
|
|
| import abc |
| import re |
| import typing |
| from typing import ( |
| TYPE_CHECKING, |
| Any, |
| Callable, |
| Final, |
| TypeVar, |
| Union, |
| ) |
|
|
| from ._ranges import ( |
| FULL_RANGE, |
| bounds_for_spec, |
| coerce_version, |
| filter_by_ranges, |
| intersect_specifier_bounds, |
| matches_bounds_only, |
| ranges_are_prerelease_only, |
| resolve_prereleases, |
| trim_release, |
| ) |
| from .utils import canonicalize_version |
| from .version import Version |
|
|
| if TYPE_CHECKING: |
| import sys |
| from collections.abc import Iterable, Iterator, Sequence |
|
|
| if sys.version_info >= (3, 10): |
| from typing import TypeGuard |
| else: |
| from typing_extensions import TypeGuard |
|
|
| from . import ranges |
| from ._ranges import Interval |
|
|
|
|
| __all__ = [ |
| "BaseSpecifier", |
| "InvalidSpecifier", |
| "Specifier", |
| "SpecifierSet", |
| ] |
|
|
|
|
| def __dir__() -> list[str]: |
| return __all__ |
|
|
|
|
| def _validate_spec(spec: object, /) -> TypeGuard[tuple[str, str]]: |
| return ( |
| isinstance(spec, tuple) |
| and len(spec) == 2 |
| and isinstance(spec[0], str) |
| and isinstance(spec[1], str) |
| ) |
|
|
|
|
| def _validate_pre(pre: object, /) -> TypeGuard[bool | None]: |
| return pre is None or isinstance(pre, bool) |
|
|
|
|
| T = TypeVar("T") |
| UnparsedVersion = Union[Version, str] |
| UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) |
|
|
|
|
| |
| |
| |
| _DIRECT_COMPARE_OPS: dict[str, Callable[[Version, Version], bool]] = { |
| ">=": Version.__ge__, |
| "<=": Version.__le__, |
| "==": Version.__eq__, |
| "!=": Version.__ne__, |
| } |
|
|
|
|
| def _fast_match(specifier: Specifier, parsed: Version) -> bool | None: |
| """Match ``parsed`` against ``specifier`` without building a range. |
| |
| Handles ``>=``, ``<=``, ``==``, ``!=``, ``<``, ``>`` when the spec is |
| not a wildcard and ``parsed`` has no local. Returns ``None`` when the |
| range path must be used. Pre-release policy is left to the caller. |
| """ |
| op_str, ver_str = specifier._spec |
| if ver_str.endswith(".*") or parsed.local is not None: |
| return None |
|
|
| direct_compare = _DIRECT_COMPARE_OPS.get(op_str) |
| if direct_compare is not None: |
| return direct_compare(parsed, specifier._require_spec_version(ver_str)) |
|
|
| if op_str in ("<", ">"): |
| spec_v = specifier._require_spec_version(ver_str) |
| |
| |
| |
| if parsed.epoch != spec_v.epoch or trim_release(parsed.release) != trim_release( |
| spec_v.release |
| ): |
| return parsed < spec_v if op_str == "<" else parsed > spec_v |
| return None |
|
|
| return 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): |
| """ |
| Abstract base class for :class:`Specifier` and :class:`SpecifierSet`. |
| """ |
|
|
| __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 |
| 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. |
| """ |
|
|
| @typing.overload |
| def filter( |
| self, |
| iterable: Iterable[UnparsedVersionVar], |
| prereleases: bool | None = None, |
| key: None = ..., |
| ) -> Iterator[UnparsedVersionVar]: ... |
|
|
| @typing.overload |
| def filter( |
| self, |
| iterable: Iterable[T], |
| prereleases: bool | None = None, |
| key: Callable[[T], UnparsedVersion] = ..., |
| ) -> Iterator[T]: ... |
|
|
| @abc.abstractmethod |
| def filter( |
| self, |
| iterable: Iterable[Any], |
| prereleases: bool | None = None, |
| key: Callable[[Any], UnparsedVersion] | None = None, |
| ) -> Iterator[Any]: |
| """ |
| 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). |
| |
| Instances are safe to serialize with :mod:`pickle`. They use a stable |
| format so the same pickle can be loaded in future packaging releases. |
| |
| .. versionchanged:: 26.2 |
| |
| Added a stable pickle format. Pickles created with packaging 26.2+ can |
| be unpickled with future releases. Backward compatibility with pickles |
| from packaging < 26.2 is supported but may be removed in a future |
| release. |
| """ |
|
|
| __slots__ = ( |
| "_prereleases", |
| "_ranges", |
| "_spec", |
| "_spec_version", |
| ) |
|
|
| _specifier_regex_str = r""" |
| (?: |
| (?: |
| # 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 .* |
| | |
| (?a: # pre release |
| [-_\.]? |
| (alpha|beta|preview|pre|a|b|c|rc) |
| [-_\.]? |
| [0-9]* |
| )? |
| (?a: # post release |
| (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) |
| )? |
| (?a:[-_\.]?dev[-_\.]?[0-9]*)? # dev release |
| (?a:\+[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. |
| (?:<=|>=|<|>) |
| |
| \s* |
| v? |
| (?:[0-9]+!)? # epoch |
| [0-9]+(?:\.[0-9]+)* # release |
| (?a: # pre release |
| [-_\.]? |
| (alpha|beta|preview|pre|a|b|c|rc) |
| [-_\.]? |
| [0-9]* |
| )? |
| (?a: # post release |
| (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) |
| )? |
| (?a:[-_\.]?dev[-_\.]?[0-9]*)? # dev release |
| ) |
| ) |
| """ |
|
|
| _regex = re.compile( |
| r"\s*" + _specifier_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). |
| """ |
| if not self._regex.fullmatch(spec): |
| raise InvalidSpecifier(f"Invalid specifier: {spec!r}") |
|
|
| spec = spec.strip() |
| if spec.startswith("==="): |
| operator, version = spec[:3], spec[3:].strip() |
| elif spec.startswith(("~=", "==", "!=", "<=", ">=")): |
| operator, version = spec[:2], spec[2:].strip() |
| else: |
| operator, version = spec[:1], spec[1:].strip() |
|
|
| self._spec: tuple[str, str] = (operator, version) |
|
|
| |
| self._prereleases = prereleases |
|
|
| |
| self._spec_version: tuple[str, Version] | None = None |
|
|
| |
| self._ranges: Sequence[Interval] | 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 |
|
|
| def _to_ranges(self) -> Sequence[Interval]: |
| """Convert this specifier to sorted, non-overlapping version ranges. |
| |
| Each standard operator maps to one or two ranges. ``===`` is |
| modeled as full range (actual check done separately). Cached. |
| """ |
| if self._ranges is not None: |
| return self._ranges |
|
|
| op = self.operator |
| ver_str = self.version |
|
|
| if op == "===": |
| result: Sequence[Interval] = FULL_RANGE |
| else: |
| version = self._require_spec_version(ver_str.removesuffix(".*")) |
| result = bounds_for_spec(op, ver_str, version) |
|
|
| self._ranges = result |
| return result |
|
|
| @property |
| def prereleases(self) -> bool | None: |
| |
| |
| if self._prereleases is not None: |
| return self._prereleases |
|
|
| |
| |
| operator, version_str = self._spec |
| if operator == "!=": |
| return False |
|
|
| |
| |
| if operator == "==" and version_str.endswith(".*"): |
| return False |
|
|
| |
| |
| version = self._get_spec_version(version_str) |
| if version is None: |
| return None |
|
|
| |
| |
| return version.is_prerelease |
|
|
| @prereleases.setter |
| def prereleases(self, value: bool | None) -> None: |
| self._prereleases = value |
|
|
| def __getstate__(self) -> tuple[tuple[str, str], bool | None]: |
| |
| |
| |
| return (self._spec, self._prereleases) |
|
|
| def __setstate__(self, state: object) -> None: |
| |
| self._spec_version = None |
| self._ranges = None |
|
|
| if isinstance(state, tuple): |
| if len(state) == 2: |
| |
| spec, prereleases = state |
| if _validate_spec(spec) and _validate_pre(prereleases): |
| self._spec = spec |
| self._prereleases = prereleases |
| return |
| if len(state) == 2 and isinstance(state[1], dict): |
| |
| _, slot_dict = state |
| spec = slot_dict.get("_spec") |
| prereleases = slot_dict.get("_prereleases", "invalid") |
| if _validate_spec(spec) and _validate_pre(prereleases): |
| self._spec = spec |
| self._prereleases = prereleases |
| return |
| if isinstance(state, dict): |
| |
| spec = state.get("_spec") |
| prereleases = state.get("_prereleases", "invalid") |
| if _validate_spec(spec) and _validate_pre(prereleases): |
| self._spec = spec |
| self._prereleases = prereleases |
| return |
|
|
| raise TypeError(f"Cannot restore Specifier from {state!r}") |
|
|
| @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 __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:`~packaging.version.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 |
| |
| .. versionchanged:: 26.0 |
| |
| With ``prereleases=None``, a prerelease now matches. A single |
| version has no alternatives, so the :pep:`440` rule to accept |
| prereleases when nothing else satisfies the specifier applies. |
| Earlier versions rejected it. An unparsable version now returns |
| ``False`` instead of raising :exc:`~packaging.version.InvalidVersion`. |
| """ |
| |
| |
| if self._spec[0] == "===": |
| return bool(list(self.filter([item], prereleases=prereleases))) |
|
|
| parsed = coerce_version(item) |
| if parsed is None: |
| |
| return False |
|
|
| if prereleases is None: |
| prereleases = resolve_prereleases(self._prereleases, self.prereleases) |
|
|
| if prereleases is False and parsed.is_prerelease: |
| return False |
|
|
| |
| |
| match = _fast_match(self, parsed) |
| if match is not None: |
| return match |
|
|
| return matches_bounds_only(self._to_ranges(), parsed) |
|
|
| @typing.overload |
| def filter( |
| self, |
| iterable: Iterable[UnparsedVersionVar], |
| prereleases: bool | None = None, |
| key: None = ..., |
| ) -> Iterator[UnparsedVersionVar]: ... |
|
|
| @typing.overload |
| def filter( |
| self, |
| iterable: Iterable[T], |
| prereleases: bool | None = None, |
| key: Callable[[T], UnparsedVersion] = ..., |
| ) -> Iterator[T]: ... |
|
|
| def filter( |
| self, |
| iterable: Iterable[Any], |
| prereleases: bool | None = None, |
| key: Callable[[Any], UnparsedVersion] | None = None, |
| ) -> Iterator[Any]: |
| """Filter items in the given iterable, that match the specifier. |
| |
| :param iterable: |
| An iterable that can contain version strings and |
| :class:`~packaging.version.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. |
| :param key: |
| A callable that takes a single argument (an item from the iterable) and |
| returns a version string or :class:`~packaging.version.Version` |
| instance to be used for filtering. |
| |
| >>> 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'] |
| >>> list(Specifier(">=1.2.3").filter( |
| ... [{"ver": "1.2"}, {"ver": "1.3"}], |
| ... key=lambda x: x["ver"])) |
| [{'ver': '1.3'}] |
| |
| .. versionchanged:: 26.1 |
| |
| Added the ``key`` parameter. |
| """ |
| if prereleases is None: |
| prereleases = resolve_prereleases(self._prereleases, self.prereleases) |
|
|
| if self.operator == "===": |
| spec_lower = self.version.lower() |
| matches = ( |
| item |
| for item in iterable |
| if str(item if key is None else key(item)).lower() == spec_lower |
| ) |
| return _apply_prereleases_filter(matches, key, prereleases) |
|
|
| return filter_by_ranges(self._to_ranges(), iterable, key, prereleases) |
|
|
|
|
| def _apply_prereleases_filter( |
| matches: Iterable[Any], |
| key: Callable[[Any], UnparsedVersion] | None, |
| prereleases: bool | None, |
| ) -> Iterator[Any]: |
| """Apply ``prereleases=`` handling to an already-matched iterable. |
| |
| ``None`` means PEP 440 default (buffer pre-releases until a final |
| appears); ``True`` yields everything; ``False`` drops pre-releases. |
| """ |
| if prereleases is None: |
| return _pep440_filter_prereleases(matches, key) |
| if prereleases: |
| return iter(matches) |
| return ( |
| item |
| for item in matches |
| if (parsed := coerce_version(item if key is None else key(item))) is None |
| or not parsed.is_prerelease |
| ) |
|
|
|
|
| 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. |
| |
| Instances are safe to serialize with :mod:`pickle`. They use a stable |
| format so the same pickle can be loaded in future packaging |
| releases. |
| |
| .. versionchanged:: 26.2 |
| |
| Added a stable pickle format. Pickles created with |
| packaging 26.2+ can be unpickled with future releases. |
| Backward compatibility with pickles from |
| packaging < 26.2 is supported but may be removed in a future |
| release. |
| """ |
|
|
| __slots__ = ( |
| "_canonicalized", |
| "_has_arbitrary", |
| "_is_unsatisfiable", |
| "_prereleases", |
| "_ranges", |
| "_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_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] |
|
|
| self._specs: tuple[Specifier, ...] = tuple(map(Specifier, split_specifiers)) |
| |
| self._has_arbitrary = "===" in specifiers |
| else: |
| self._specs = tuple(specifiers) |
| |
| |
| self._has_arbitrary = any("===" in str(s) for s in self._specs) |
|
|
| self._canonicalized = len(self._specs) <= 1 |
| self._is_unsatisfiable: bool | None = None |
| self._ranges: Sequence[Interval] | None = None |
|
|
| |
| |
| self._prereleases = prereleases |
|
|
| def _canonical_specs(self) -> tuple[Specifier, ...]: |
| """Deduplicate, sort, and cache specs for order-sensitive operations.""" |
| if not self._canonicalized: |
| self._specs = tuple(dict.fromkeys(sorted(self._specs, key=str))) |
| self._canonicalized = True |
| return self._specs |
|
|
| @property |
| def prereleases(self) -> bool | None: |
| |
| |
| if self._prereleases is not None: |
| return self._prereleases |
|
|
| |
| |
| |
| if not self._specs: |
| return None |
|
|
| |
| |
| 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 |
| self._is_unsatisfiable = None |
|
|
| def __getstate__(self) -> tuple[tuple[Specifier, ...], bool | None]: |
| |
| |
| |
| return (self._specs, self._prereleases) |
|
|
| def __setstate__(self, state: object) -> None: |
| |
| self._ranges = None |
| self._is_unsatisfiable = None |
|
|
| if isinstance(state, tuple): |
| if len(state) == 2: |
| |
| specs, prereleases = state |
| if ( |
| isinstance(specs, tuple) |
| and all(isinstance(s, Specifier) for s in specs) |
| and _validate_pre(prereleases) |
| ): |
| self._specs = specs |
| self._prereleases = prereleases |
| self._canonicalized = len(specs) <= 1 |
| self._has_arbitrary = any("===" in str(s) for s in specs) |
| return |
| if len(state) == 2 and isinstance(state[1], dict): |
| |
| _, slot_dict = state |
| specs = slot_dict.get("_specs", ()) |
| prereleases = slot_dict.get("_prereleases") |
| |
| if isinstance(specs, frozenset): |
| specs = tuple(sorted(specs, key=str)) |
| if ( |
| isinstance(specs, tuple) |
| and all(isinstance(s, Specifier) for s in specs) |
| and _validate_pre(prereleases) |
| ): |
| self._specs = specs |
| self._prereleases = prereleases |
| self._canonicalized = len(self._specs) <= 1 |
| self._has_arbitrary = any("===" in str(s) for s in self._specs) |
| return |
| if isinstance(state, dict): |
| |
| specs = state.get("_specs", ()) |
| prereleases = state.get("_prereleases") |
| |
| if isinstance(specs, frozenset): |
| specs = tuple(sorted(specs, key=str)) |
| if ( |
| isinstance(specs, tuple) |
| and all(isinstance(s, Specifier) for s in specs) |
| and _validate_pre(prereleases) |
| ): |
| self._specs = specs |
| self._prereleases = prereleases |
| self._canonicalized = len(self._specs) <= 1 |
| self._has_arbitrary = any("===" in str(s) for s in self._specs) |
| return |
|
|
| raise TypeError(f"Cannot restore SpecifierSet from {state!r}") |
|
|
| 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"<{self.__class__.__name__}({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(str(s) for s in self._canonical_specs()) |
|
|
| def __hash__(self) -> int: |
| return hash(self._canonical_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 = self._specs + other._specs |
| specifier._canonicalized = len(specifier._specs) <= 1 |
| specifier._has_arbitrary = self._has_arbitrary or other._has_arbitrary |
|
|
| |
| if self._prereleases is None or self._prereleases == other._prereleases: |
| specifier._prereleases = other._prereleases |
| elif other._prereleases is None: |
| 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._canonical_specs() == other._canonical_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 _get_ranges(self) -> Sequence[Interval]: |
| """Intersect all specifiers into a single sequence of version ranges. |
| |
| Empty when unsatisfiable. Callers must ensure ``self._specs`` |
| is non-empty. |
| """ |
| if self._ranges is not None: |
| return self._ranges |
|
|
| self._ranges = intersect_specifier_bounds(s._to_ranges() for s in self._specs) |
| return self._ranges |
|
|
| def is_unsatisfiable(self) -> bool: |
| """Check whether this specifier set can never be satisfied. |
| |
| Returns True if no version can satisfy all specifiers simultaneously. |
| |
| >>> SpecifierSet(">=2.0,<1.0").is_unsatisfiable() |
| True |
| >>> SpecifierSet(">=1.0,<2.0").is_unsatisfiable() |
| False |
| >>> SpecifierSet("").is_unsatisfiable() |
| False |
| >>> SpecifierSet("==1.0,!=1.0").is_unsatisfiable() |
| True |
| |
| .. versionadded:: 26.1 |
| """ |
| cached = self._is_unsatisfiable |
| if cached is not None: |
| return cached |
|
|
| if not self._specs: |
| self._is_unsatisfiable = False |
| return False |
|
|
| result = not self._get_ranges() |
|
|
| if not result: |
| result = self._check_arbitrary_unsatisfiable() |
|
|
| if not result and self.prereleases is False: |
| result = ranges_are_prerelease_only(self._get_ranges()) |
|
|
| self._is_unsatisfiable = result |
| return result |
|
|
| def _check_arbitrary_unsatisfiable(self) -> bool: |
| """Check === (arbitrary equality) specs for unsatisfiability. |
| |
| === uses case-insensitive string comparison, so the only candidate |
| that can match ``===V`` is the literal string V. This method |
| checks whether that candidate is excluded by other specifiers. |
| """ |
| arbitrary = [s for s in self._specs if s.operator == "==="] |
| if not arbitrary: |
| return False |
|
|
| |
| first = arbitrary[0].version.lower() |
| if any(s.version.lower() != first for s in arbitrary[1:]): |
| return True |
|
|
| |
| |
| candidate = coerce_version(arbitrary[0].version) |
|
|
| |
| |
| if ( |
| self.prereleases is False |
| and candidate is not None |
| and candidate.is_prerelease |
| ): |
| return True |
|
|
| standard = [s for s in self._specs if s.operator != "==="] |
| if not standard: |
| return False |
|
|
| if candidate is None: |
| |
| return True |
|
|
| return not all(s.contains(candidate) for s in standard) |
|
|
| def to_range(self) -> ranges.VersionRange: |
| """Return the :class:`~packaging.ranges.VersionRange` this set accepts. |
| |
| An empty set yields the full range; an unsatisfiable set yields the |
| empty range. ``===`` specifiers contribute literal-string admission. |
| |
| >>> SpecifierSet(">=1.0,<2.0").to_range() |
| <VersionRange '[1.0, 2.0.dev0)'> |
| |
| .. versionadded:: 26.3 |
| """ |
| from .ranges import VersionRange |
|
|
| return VersionRange._from_specifier_set(self) |
|
|
| def _check_relation_operand(self, other: object) -> None: |
| if not isinstance(other, SpecifierSet): |
| raise TypeError("expected a SpecifierSet") |
| if self._has_arbitrary or other._has_arbitrary: |
| raise ValueError("set relations do not support === specifiers") |
|
|
| def is_subset(self, other: SpecifierSet) -> bool: |
| """Return whether every version matching this set also matches other. |
| |
| :raises ValueError: |
| If either set uses ``===`` specifiers, or the two sets were |
| given different ``prereleases`` arguments (unset on one side |
| counts as different). |
| :raises TypeError: |
| If other is not a :class:`SpecifierSet`. |
| |
| >>> SpecifierSet(">=3.12,<3.13").is_subset(SpecifierSet(">=3.12")) |
| True |
| >>> SpecifierSet(">=3.12").is_subset(SpecifierSet(">=3.12,<3.13")) |
| False |
| |
| .. versionadded:: 26.3 |
| """ |
| self._check_relation_operand(other) |
| return self.to_range().is_subset(other.to_range()) |
|
|
| def is_superset(self, other: SpecifierSet) -> bool: |
| """Return whether every version matching other also matches this set. |
| |
| :raises ValueError: |
| If either set uses ``===`` specifiers, or the two sets were |
| given different ``prereleases`` arguments (unset on one side |
| counts as different). |
| :raises TypeError: |
| If other is not a :class:`SpecifierSet`. |
| |
| >>> SpecifierSet(">=3.12").is_superset(SpecifierSet(">=3.12,<3.13")) |
| True |
| |
| .. versionadded:: 26.3 |
| """ |
| self._check_relation_operand(other) |
| return self.to_range().is_superset(other.to_range()) |
|
|
| def is_disjoint(self, other: SpecifierSet) -> bool: |
| """Return whether this set and other share no matching versions. |
| |
| :raises ValueError: |
| If either set uses ``===`` specifiers, or the two sets were |
| given different ``prereleases`` arguments (unset on one side |
| counts as different). |
| :raises TypeError: |
| If other is not a :class:`SpecifierSet`. |
| |
| >>> SpecifierSet("<3.12").is_disjoint(SpecifierSet(">=3.12")) |
| True |
| >>> SpecifierSet("<3.12").is_disjoint(SpecifierSet(">=3.11")) |
| False |
| |
| .. versionadded:: 26.3 |
| """ |
| self._check_relation_operand(other) |
| return self.to_range().is_disjoint(other.to_range()) |
|
|
| 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:`~packaging.version.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 |
| |
| .. versionchanged:: 26.0 |
| |
| With ``prereleases=None``, a prerelease now matches. A single |
| version has no alternatives, so the :pep:`440` rule to accept |
| prereleases when nothing else satisfies the specifiers applies. |
| Earlier versions rejected it. An unparsable version now returns |
| ``False`` instead of raising :exc:`~packaging.version.InvalidVersion`. |
| """ |
| version = coerce_version(item) |
|
|
| if version is not None and installed and version.is_prerelease: |
| prereleases = True |
|
|
| |
| |
| if version is None or (self._has_arbitrary and not isinstance(item, Version)): |
| check_item = item |
| else: |
| check_item = version |
|
|
| |
| |
| |
| if ( |
| version is not None |
| and not self._has_arbitrary |
| and version.local is None |
| and self._specs |
| ): |
| if version.is_prerelease and ( |
| prereleases is False |
| or (prereleases is None and self._prereleases is False) |
| ): |
| return False |
|
|
| bounds = self._ranges |
| if bounds is None: |
| |
| |
| |
| |
| for spec in self._specs: |
| match = _fast_match(spec, version) |
| if match is None: |
| break |
| if not match: |
| return False |
| else: |
| return True |
|
|
| bounds = self._ranges = self._get_ranges() |
|
|
| return matches_bounds_only(bounds, version) |
|
|
| return bool(list(self.filter([check_item], prereleases=prereleases))) |
|
|
| @typing.overload |
| def filter( |
| self, |
| iterable: Iterable[UnparsedVersionVar], |
| prereleases: bool | None = None, |
| key: None = ..., |
| ) -> Iterator[UnparsedVersionVar]: ... |
|
|
| @typing.overload |
| def filter( |
| self, |
| iterable: Iterable[T], |
| prereleases: bool | None = None, |
| key: Callable[[T], UnparsedVersion] = ..., |
| ) -> Iterator[T]: ... |
|
|
| def filter( |
| self, |
| iterable: Iterable[Any], |
| prereleases: bool | None = None, |
| key: Callable[[Any], UnparsedVersion] | None = None, |
| ) -> Iterator[Any]: |
| """Filter items in the given iterable, that match the specifiers in this set. |
| |
| :param iterable: |
| An iterable that can contain version strings and |
| :class:`~packaging.version.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. |
| :param key: |
| A callable that takes a single argument (an item from the iterable) and |
| returns a version string or :class:`~packaging.version.Version` |
| instance to be used for filtering. |
| |
| >>> 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'] |
| >>> list(SpecifierSet(">=1.2.3").filter( |
| ... [{"ver": "1.2"}, {"ver": "1.3"}], |
| ... key=lambda x: x["ver"])) |
| [{'ver': '1.3'}] |
| |
| 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'] |
| |
| .. versionchanged:: 26.0 |
| |
| Prerelease filtering now follows the PEP 440 recommendation of |
| yielding prereleases only when no final release is present. |
| |
| .. versionchanged:: 26.1 |
| |
| Added the ``key`` parameter. |
| """ |
| |
| |
| |
| if prereleases is None and self.prereleases is not None: |
| prereleases = self.prereleases |
|
|
| if self._specs: |
| if self._has_arbitrary: |
| |
| specs = self._specs |
| matches = ( |
| item |
| for item in iterable |
| if all( |
| s.contains(item if key is None else key(item), prereleases=True) |
| for s in specs |
| ) |
| ) |
| return _apply_prereleases_filter(matches, key, prereleases) |
|
|
| ranges = self._ranges |
| if ranges is None: |
| ranges = self._get_ranges() |
| return filter_by_ranges(ranges, iterable, key, prereleases) |
|
|
| |
| return _apply_prereleases_filter(iterable, key, prereleases) |
|
|
|
|
| def _pep440_filter_prereleases( |
| iterable: Iterable[Any], key: Callable[[Any], UnparsedVersion] | None |
| ) -> Iterator[Any]: |
| """Filter per PEP 440: exclude prereleases unless no finals exist.""" |
| |
| |
| |
| all_nonfinal: list[Any] = [] |
| arbitrary_strings: list[Any] = [] |
|
|
| found_final = False |
| for item in iterable: |
| parsed = coerce_version(item if key is None else key(item)) |
|
|
| if parsed is None: |
| |
| |
| |
| if found_final: |
| yield item |
| else: |
| arbitrary_strings.append(item) |
| all_nonfinal.append(item) |
| continue |
|
|
| if not parsed.is_prerelease: |
| |
| if not found_final: |
| yield from arbitrary_strings |
| found_final = True |
| yield item |
| continue |
|
|
| |
| if not found_final: |
| all_nonfinal.append(item) |
|
|
| |
| if not found_final: |
| yield from all_nonfinal |
|
|