gmake_python_helpers

Helpers for using Python venv/pip-tools/black/pylama/... with GNU make
git clone https://ccx.te2000.cz/git/gmake_python_helpers
Log | Files | Refs | README

specifiers.py (40074B)


      1 # This file is dual licensed under the terms of the Apache License, Version
      2 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
      3 # for complete details.
      4 """
      5 .. testsetup::
      6 
      7     from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier
      8     from packaging.version import Version
      9 """
     10 
     11 from __future__ import annotations
     12 
     13 import abc
     14 import itertools
     15 import re
     16 from typing import Callable, Iterable, Iterator, TypeVar, Union
     17 
     18 from .utils import canonicalize_version
     19 from .version import Version
     20 
     21 UnparsedVersion = Union[Version, str]
     22 UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion)
     23 CallableOperator = Callable[[Version, str], bool]
     24 
     25 
     26 def _coerce_version(version: UnparsedVersion) -> Version:
     27     if not isinstance(version, Version):
     28         version = Version(version)
     29     return version
     30 
     31 
     32 class InvalidSpecifier(ValueError):
     33     """
     34     Raised when attempting to create a :class:`Specifier` with a specifier
     35     string that is invalid.
     36 
     37     >>> Specifier("lolwat")
     38     Traceback (most recent call last):
     39         ...
     40     packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat'
     41     """
     42 
     43 
     44 class BaseSpecifier(metaclass=abc.ABCMeta):
     45     @abc.abstractmethod
     46     def __str__(self) -> str:
     47         """
     48         Returns the str representation of this Specifier-like object. This
     49         should be representative of the Specifier itself.
     50         """
     51 
     52     @abc.abstractmethod
     53     def __hash__(self) -> int:
     54         """
     55         Returns a hash value for this Specifier-like object.
     56         """
     57 
     58     @abc.abstractmethod
     59     def __eq__(self, other: object) -> bool:
     60         """
     61         Returns a boolean representing whether or not the two Specifier-like
     62         objects are equal.
     63 
     64         :param other: The other object to check against.
     65         """
     66 
     67     @property
     68     @abc.abstractmethod
     69     def prereleases(self) -> bool | None:
     70         """Whether or not pre-releases as a whole are allowed.
     71 
     72         This can be set to either ``True`` or ``False`` to explicitly enable or disable
     73         prereleases or it can be set to ``None`` (the default) to use default semantics.
     74         """
     75 
     76     @prereleases.setter
     77     def prereleases(self, value: bool) -> None:
     78         """Setter for :attr:`prereleases`.
     79 
     80         :param value: The value to set.
     81         """
     82 
     83     @abc.abstractmethod
     84     def contains(self, item: str, prereleases: bool | None = None) -> bool:
     85         """
     86         Determines if the given item is contained within this specifier.
     87         """
     88 
     89     @abc.abstractmethod
     90     def filter(
     91         self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
     92     ) -> Iterator[UnparsedVersionVar]:
     93         """
     94         Takes an iterable of items and filters them so that only items which
     95         are contained within this specifier are allowed in it.
     96         """
     97 
     98 
     99 class Specifier(BaseSpecifier):
    100     """This class abstracts handling of version specifiers.
    101 
    102     .. tip::
    103 
    104         It is generally not required to instantiate this manually. You should instead
    105         prefer to work with :class:`SpecifierSet` instead, which can parse
    106         comma-separated version specifiers (which is what package metadata contains).
    107     """
    108 
    109     _operator_regex_str = r"""
    110         (?P<operator>(~=|==|!=|<=|>=|<|>|===))
    111         """
    112     _version_regex_str = r"""
    113         (?P<version>
    114             (?:
    115                 # The identity operators allow for an escape hatch that will
    116                 # do an exact string match of the version you wish to install.
    117                 # This will not be parsed by PEP 440 and we cannot determine
    118                 # any semantic meaning from it. This operator is discouraged
    119                 # but included entirely as an escape hatch.
    120                 (?<====)  # Only match for the identity operator
    121                 \s*
    122                 [^\s;)]*  # The arbitrary version can be just about anything,
    123                           # we match everything except for whitespace, a
    124                           # semi-colon for marker support, and a closing paren
    125                           # since versions can be enclosed in them.
    126             )
    127             |
    128             (?:
    129                 # The (non)equality operators allow for wild card and local
    130                 # versions to be specified so we have to define these two
    131                 # operators separately to enable that.
    132                 (?<===|!=)            # Only match for equals and not equals
    133 
    134                 \s*
    135                 v?
    136                 (?:[0-9]+!)?          # epoch
    137                 [0-9]+(?:\.[0-9]+)*   # release
    138 
    139                 # You cannot use a wild card and a pre-release, post-release, a dev or
    140                 # local version together so group them with a | and make them optional.
    141                 (?:
    142                     \.\*  # Wild card syntax of .*
    143                     |
    144                     (?:                                  # pre release
    145                         [-_\.]?
    146                         (alpha|beta|preview|pre|a|b|c|rc)
    147                         [-_\.]?
    148                         [0-9]*
    149                     )?
    150                     (?:                                  # post release
    151                         (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
    152                     )?
    153                     (?:[-_\.]?dev[-_\.]?[0-9]*)?         # dev release
    154                     (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
    155                 )?
    156             )
    157             |
    158             (?:
    159                 # The compatible operator requires at least two digits in the
    160                 # release segment.
    161                 (?<=~=)               # Only match for the compatible operator
    162 
    163                 \s*
    164                 v?
    165                 (?:[0-9]+!)?          # epoch
    166                 [0-9]+(?:\.[0-9]+)+   # release  (We have a + instead of a *)
    167                 (?:                   # pre release
    168                     [-_\.]?
    169                     (alpha|beta|preview|pre|a|b|c|rc)
    170                     [-_\.]?
    171                     [0-9]*
    172                 )?
    173                 (?:                                   # post release
    174                     (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
    175                 )?
    176                 (?:[-_\.]?dev[-_\.]?[0-9]*)?          # dev release
    177             )
    178             |
    179             (?:
    180                 # All other operators only allow a sub set of what the
    181                 # (non)equality operators do. Specifically they do not allow
    182                 # local versions to be specified nor do they allow the prefix
    183                 # matching wild cards.
    184                 (?<!==|!=|~=)         # We have special cases for these
    185                                       # operators so we want to make sure they
    186                                       # don't match here.
    187 
    188                 \s*
    189                 v?
    190                 (?:[0-9]+!)?          # epoch
    191                 [0-9]+(?:\.[0-9]+)*   # release
    192                 (?:                   # pre release
    193                     [-_\.]?
    194                     (alpha|beta|preview|pre|a|b|c|rc)
    195                     [-_\.]?
    196                     [0-9]*
    197                 )?
    198                 (?:                                   # post release
    199                     (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
    200                 )?
    201                 (?:[-_\.]?dev[-_\.]?[0-9]*)?          # dev release
    202             )
    203         )
    204         """
    205 
    206     _regex = re.compile(
    207         r"^\s*" + _operator_regex_str + _version_regex_str + r"\s*$",
    208         re.VERBOSE | re.IGNORECASE,
    209     )
    210 
    211     _operators = {
    212         "~=": "compatible",
    213         "==": "equal",
    214         "!=": "not_equal",
    215         "<=": "less_than_equal",
    216         ">=": "greater_than_equal",
    217         "<": "less_than",
    218         ">": "greater_than",
    219         "===": "arbitrary",
    220     }
    221 
    222     def __init__(self, spec: str = "", prereleases: bool | None = None) -> None:
    223         """Initialize a Specifier instance.
    224 
    225         :param spec:
    226             The string representation of a specifier which will be parsed and
    227             normalized before use.
    228         :param prereleases:
    229             This tells the specifier if it should accept prerelease versions if
    230             applicable or not. The default of ``None`` will autodetect it from the
    231             given specifiers.
    232         :raises InvalidSpecifier:
    233             If the given specifier is invalid (i.e. bad syntax).
    234         """
    235         match = self._regex.search(spec)
    236         if not match:
    237             raise InvalidSpecifier(f"Invalid specifier: {spec!r}")
    238 
    239         self._spec: tuple[str, str] = (
    240             match.group("operator").strip(),
    241             match.group("version").strip(),
    242         )
    243 
    244         # Store whether or not this Specifier should accept prereleases
    245         self._prereleases = prereleases
    246 
    247     # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515
    248     @property  # type: ignore[override]
    249     def prereleases(self) -> bool:
    250         # If there is an explicit prereleases set for this, then we'll just
    251         # blindly use that.
    252         if self._prereleases is not None:
    253             return self._prereleases
    254 
    255         # Look at all of our specifiers and determine if they are inclusive
    256         # operators, and if they are if they are including an explicit
    257         # prerelease.
    258         operator, version = self._spec
    259         if operator in ["==", ">=", "<=", "~=", "===", ">", "<"]:
    260             # The == specifier can include a trailing .*, if it does we
    261             # want to remove before parsing.
    262             if operator == "==" and version.endswith(".*"):
    263                 version = version[:-2]
    264 
    265             # Parse the version, and if it is a pre-release than this
    266             # specifier allows pre-releases.
    267             if Version(version).is_prerelease:
    268                 return True
    269 
    270         return False
    271 
    272     @prereleases.setter
    273     def prereleases(self, value: bool) -> None:
    274         self._prereleases = value
    275 
    276     @property
    277     def operator(self) -> str:
    278         """The operator of this specifier.
    279 
    280         >>> Specifier("==1.2.3").operator
    281         '=='
    282         """
    283         return self._spec[0]
    284 
    285     @property
    286     def version(self) -> str:
    287         """The version of this specifier.
    288 
    289         >>> Specifier("==1.2.3").version
    290         '1.2.3'
    291         """
    292         return self._spec[1]
    293 
    294     def __repr__(self) -> str:
    295         """A representation of the Specifier that shows all internal state.
    296 
    297         >>> Specifier('>=1.0.0')
    298         <Specifier('>=1.0.0')>
    299         >>> Specifier('>=1.0.0', prereleases=False)
    300         <Specifier('>=1.0.0', prereleases=False)>
    301         >>> Specifier('>=1.0.0', prereleases=True)
    302         <Specifier('>=1.0.0', prereleases=True)>
    303         """
    304         pre = (
    305             f", prereleases={self.prereleases!r}"
    306             if self._prereleases is not None
    307             else ""
    308         )
    309 
    310         return f"<{self.__class__.__name__}({str(self)!r}{pre})>"
    311 
    312     def __str__(self) -> str:
    313         """A string representation of the Specifier that can be round-tripped.
    314 
    315         >>> str(Specifier('>=1.0.0'))
    316         '>=1.0.0'
    317         >>> str(Specifier('>=1.0.0', prereleases=False))
    318         '>=1.0.0'
    319         """
    320         return "{}{}".format(*self._spec)
    321 
    322     @property
    323     def _canonical_spec(self) -> tuple[str, str]:
    324         canonical_version = canonicalize_version(
    325             self._spec[1],
    326             strip_trailing_zero=(self._spec[0] != "~="),
    327         )
    328         return self._spec[0], canonical_version
    329 
    330     def __hash__(self) -> int:
    331         return hash(self._canonical_spec)
    332 
    333     def __eq__(self, other: object) -> bool:
    334         """Whether or not the two Specifier-like objects are equal.
    335 
    336         :param other: The other object to check against.
    337 
    338         The value of :attr:`prereleases` is ignored.
    339 
    340         >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0")
    341         True
    342         >>> (Specifier("==1.2.3", prereleases=False) ==
    343         ...  Specifier("==1.2.3", prereleases=True))
    344         True
    345         >>> Specifier("==1.2.3") == "==1.2.3"
    346         True
    347         >>> Specifier("==1.2.3") == Specifier("==1.2.4")
    348         False
    349         >>> Specifier("==1.2.3") == Specifier("~=1.2.3")
    350         False
    351         """
    352         if isinstance(other, str):
    353             try:
    354                 other = self.__class__(str(other))
    355             except InvalidSpecifier:
    356                 return NotImplemented
    357         elif not isinstance(other, self.__class__):
    358             return NotImplemented
    359 
    360         return self._canonical_spec == other._canonical_spec
    361 
    362     def _get_operator(self, op: str) -> CallableOperator:
    363         operator_callable: CallableOperator = getattr(
    364             self, f"_compare_{self._operators[op]}"
    365         )
    366         return operator_callable
    367 
    368     def _compare_compatible(self, prospective: Version, spec: str) -> bool:
    369         # Compatible releases have an equivalent combination of >= and ==. That
    370         # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
    371         # implement this in terms of the other specifiers instead of
    372         # implementing it ourselves. The only thing we need to do is construct
    373         # the other specifiers.
    374 
    375         # We want everything but the last item in the version, but we want to
    376         # ignore suffix segments.
    377         prefix = _version_join(
    378             list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1]
    379         )
    380 
    381         # Add the prefix notation to the end of our string
    382         prefix += ".*"
    383 
    384         return self._get_operator(">=")(prospective, spec) and self._get_operator("==")(
    385             prospective, prefix
    386         )
    387 
    388     def _compare_equal(self, prospective: Version, spec: str) -> bool:
    389         # We need special logic to handle prefix matching
    390         if spec.endswith(".*"):
    391             # In the case of prefix matching we want to ignore local segment.
    392             normalized_prospective = canonicalize_version(
    393                 prospective.public, strip_trailing_zero=False
    394             )
    395             # Get the normalized version string ignoring the trailing .*
    396             normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
    397             # Split the spec out by bangs and dots, and pretend that there is
    398             # an implicit dot in between a release segment and a pre-release segment.
    399             split_spec = _version_split(normalized_spec)
    400 
    401             # Split the prospective version out by bangs and dots, and pretend
    402             # that there is an implicit dot in between a release segment and
    403             # a pre-release segment.
    404             split_prospective = _version_split(normalized_prospective)
    405 
    406             # 0-pad the prospective version before shortening it to get the correct
    407             # shortened version.
    408             padded_prospective, _ = _pad_version(split_prospective, split_spec)
    409 
    410             # Shorten the prospective version to be the same length as the spec
    411             # so that we can determine if the specifier is a prefix of the
    412             # prospective version or not.
    413             shortened_prospective = padded_prospective[: len(split_spec)]
    414 
    415             return shortened_prospective == split_spec
    416         else:
    417             # Convert our spec string into a Version
    418             spec_version = Version(spec)
    419 
    420             # If the specifier does not have a local segment, then we want to
    421             # act as if the prospective version also does not have a local
    422             # segment.
    423             if not spec_version.local:
    424                 prospective = Version(prospective.public)
    425 
    426             return prospective == spec_version
    427 
    428     def _compare_not_equal(self, prospective: Version, spec: str) -> bool:
    429         return not self._compare_equal(prospective, spec)
    430 
    431     def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
    432         # NB: Local version identifiers are NOT permitted in the version
    433         # specifier, so local version labels can be universally removed from
    434         # the prospective version.
    435         return Version(prospective.public) <= Version(spec)
    436 
    437     def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
    438         # NB: Local version identifiers are NOT permitted in the version
    439         # specifier, so local version labels can be universally removed from
    440         # the prospective version.
    441         return Version(prospective.public) >= Version(spec)
    442 
    443     def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
    444         # Convert our spec to a Version instance, since we'll want to work with
    445         # it as a version.
    446         spec = Version(spec_str)
    447 
    448         # Check to see if the prospective version is less than the spec
    449         # version. If it's not we can short circuit and just return False now
    450         # instead of doing extra unneeded work.
    451         if not prospective < spec:
    452             return False
    453 
    454         # This special case is here so that, unless the specifier itself
    455         # includes is a pre-release version, that we do not accept pre-release
    456         # versions for the version mentioned in the specifier (e.g. <3.1 should
    457         # not match 3.1.dev0, but should match 3.0.dev0).
    458         if not spec.is_prerelease and prospective.is_prerelease:
    459             if Version(prospective.base_version) == Version(spec.base_version):
    460                 return False
    461 
    462         # If we've gotten to here, it means that prospective version is both
    463         # less than the spec version *and* it's not a pre-release of the same
    464         # version in the spec.
    465         return True
    466 
    467     def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
    468         # Convert our spec to a Version instance, since we'll want to work with
    469         # it as a version.
    470         spec = Version(spec_str)
    471 
    472         # Check to see if the prospective version is greater than the spec
    473         # version. If it's not we can short circuit and just return False now
    474         # instead of doing extra unneeded work.
    475         if not prospective > spec:
    476             return False
    477 
    478         # This special case is here so that, unless the specifier itself
    479         # includes is a post-release version, that we do not accept
    480         # post-release versions for the version mentioned in the specifier
    481         # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0).
    482         if not spec.is_postrelease and prospective.is_postrelease:
    483             if Version(prospective.base_version) == Version(spec.base_version):
    484                 return False
    485 
    486         # Ensure that we do not allow a local version of the version mentioned
    487         # in the specifier, which is technically greater than, to match.
    488         if prospective.local is not None:
    489             if Version(prospective.base_version) == Version(spec.base_version):
    490                 return False
    491 
    492         # If we've gotten to here, it means that prospective version is both
    493         # greater than the spec version *and* it's not a pre-release of the
    494         # same version in the spec.
    495         return True
    496 
    497     def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
    498         return str(prospective).lower() == str(spec).lower()
    499 
    500     def __contains__(self, item: str | Version) -> bool:
    501         """Return whether or not the item is contained in this specifier.
    502 
    503         :param item: The item to check for.
    504 
    505         This is used for the ``in`` operator and behaves the same as
    506         :meth:`contains` with no ``prereleases`` argument passed.
    507 
    508         >>> "1.2.3" in Specifier(">=1.2.3")
    509         True
    510         >>> Version("1.2.3") in Specifier(">=1.2.3")
    511         True
    512         >>> "1.0.0" in Specifier(">=1.2.3")
    513         False
    514         >>> "1.3.0a1" in Specifier(">=1.2.3")
    515         False
    516         >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True)
    517         True
    518         """
    519         return self.contains(item)
    520 
    521     def contains(self, item: UnparsedVersion, prereleases: bool | None = None) -> bool:
    522         """Return whether or not the item is contained in this specifier.
    523 
    524         :param item:
    525             The item to check for, which can be a version string or a
    526             :class:`Version` instance.
    527         :param prereleases:
    528             Whether or not to match prereleases with this Specifier. If set to
    529             ``None`` (the default), it uses :attr:`prereleases` to determine
    530             whether or not prereleases are allowed.
    531 
    532         >>> Specifier(">=1.2.3").contains("1.2.3")
    533         True
    534         >>> Specifier(">=1.2.3").contains(Version("1.2.3"))
    535         True
    536         >>> Specifier(">=1.2.3").contains("1.0.0")
    537         False
    538         >>> Specifier(">=1.2.3").contains("1.3.0a1")
    539         False
    540         >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1")
    541         True
    542         >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True)
    543         True
    544         """
    545 
    546         # Determine if prereleases are to be allowed or not.
    547         if prereleases is None:
    548             prereleases = self.prereleases
    549 
    550         # Normalize item to a Version, this allows us to have a shortcut for
    551         # "2.0" in Specifier(">=2")
    552         normalized_item = _coerce_version(item)
    553 
    554         # Determine if we should be supporting prereleases in this specifier
    555         # or not, if we do not support prereleases than we can short circuit
    556         # logic if this version is a prereleases.
    557         if normalized_item.is_prerelease and not prereleases:
    558             return False
    559 
    560         # Actually do the comparison to determine if this item is contained
    561         # within this Specifier or not.
    562         operator_callable: CallableOperator = self._get_operator(self.operator)
    563         return operator_callable(normalized_item, self.version)
    564 
    565     def filter(
    566         self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
    567     ) -> Iterator[UnparsedVersionVar]:
    568         """Filter items in the given iterable, that match the specifier.
    569 
    570         :param iterable:
    571             An iterable that can contain version strings and :class:`Version` instances.
    572             The items in the iterable will be filtered according to the specifier.
    573         :param prereleases:
    574             Whether or not to allow prereleases in the returned iterator. If set to
    575             ``None`` (the default), it will be intelligently decide whether to allow
    576             prereleases or not (based on the :attr:`prereleases` attribute, and
    577             whether the only versions matching are prereleases).
    578 
    579         This method is smarter than just ``filter(Specifier().contains, [...])``
    580         because it implements the rule from :pep:`440` that a prerelease item
    581         SHOULD be accepted if no other versions match the given specifier.
    582 
    583         >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"]))
    584         ['1.3']
    585         >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")]))
    586         ['1.2.3', '1.3', <Version('1.4')>]
    587         >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"]))
    588         ['1.5a1']
    589         >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True))
    590         ['1.3', '1.5a1']
    591         >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"]))
    592         ['1.3', '1.5a1']
    593         """
    594 
    595         yielded = False
    596         found_prereleases = []
    597 
    598         kw = {"prereleases": prereleases if prereleases is not None else True}
    599 
    600         # Attempt to iterate over all the values in the iterable and if any of
    601         # them match, yield them.
    602         for version in iterable:
    603             parsed_version = _coerce_version(version)
    604 
    605             if self.contains(parsed_version, **kw):
    606                 # If our version is a prerelease, and we were not set to allow
    607                 # prereleases, then we'll store it for later in case nothing
    608                 # else matches this specifier.
    609                 if parsed_version.is_prerelease and not (
    610                     prereleases or self.prereleases
    611                 ):
    612                     found_prereleases.append(version)
    613                 # Either this is not a prerelease, or we should have been
    614                 # accepting prereleases from the beginning.
    615                 else:
    616                     yielded = True
    617                     yield version
    618 
    619         # Now that we've iterated over everything, determine if we've yielded
    620         # any values, and if we have not and we have any prereleases stored up
    621         # then we will go ahead and yield the prereleases.
    622         if not yielded and found_prereleases:
    623             for version in found_prereleases:
    624                 yield version
    625 
    626 
    627 _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
    628 
    629 
    630 def _version_split(version: str) -> list[str]:
    631     """Split version into components.
    632 
    633     The split components are intended for version comparison. The logic does
    634     not attempt to retain the original version string, so joining the
    635     components back with :func:`_version_join` may not produce the original
    636     version string.
    637     """
    638     result: list[str] = []
    639 
    640     epoch, _, rest = version.rpartition("!")
    641     result.append(epoch or "0")
    642 
    643     for item in rest.split("."):
    644         match = _prefix_regex.search(item)
    645         if match:
    646             result.extend(match.groups())
    647         else:
    648             result.append(item)
    649     return result
    650 
    651 
    652 def _version_join(components: list[str]) -> str:
    653     """Join split version components into a version string.
    654 
    655     This function assumes the input came from :func:`_version_split`, where the
    656     first component must be the epoch (either empty or numeric), and all other
    657     components numeric.
    658     """
    659     epoch, *rest = components
    660     return f"{epoch}!{'.'.join(rest)}"
    661 
    662 
    663 def _is_not_suffix(segment: str) -> bool:
    664     return not any(
    665         segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
    666     )
    667 
    668 
    669 def _pad_version(left: list[str], right: list[str]) -> tuple[list[str], list[str]]:
    670     left_split, right_split = [], []
    671 
    672     # Get the release segment of our versions
    673     left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
    674     right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
    675 
    676     # Get the rest of our versions
    677     left_split.append(left[len(left_split[0]) :])
    678     right_split.append(right[len(right_split[0]) :])
    679 
    680     # Insert our padding
    681     left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0])))
    682     right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0])))
    683 
    684     return (
    685         list(itertools.chain.from_iterable(left_split)),
    686         list(itertools.chain.from_iterable(right_split)),
    687     )
    688 
    689 
    690 class SpecifierSet(BaseSpecifier):
    691     """This class abstracts handling of a set of version specifiers.
    692 
    693     It can be passed a single specifier (``>=3.0``), a comma-separated list of
    694     specifiers (``>=3.0,!=3.1``), or no specifier at all.
    695     """
    696 
    697     def __init__(
    698         self,
    699         specifiers: str | Iterable[Specifier] = "",
    700         prereleases: bool | None = None,
    701     ) -> None:
    702         """Initialize a SpecifierSet instance.
    703 
    704         :param specifiers:
    705             The string representation of a specifier or a comma-separated list of
    706             specifiers which will be parsed and normalized before use.
    707             May also be an iterable of ``Specifier`` instances, which will be used
    708             as is.
    709         :param prereleases:
    710             This tells the SpecifierSet if it should accept prerelease versions if
    711             applicable or not. The default of ``None`` will autodetect it from the
    712             given specifiers.
    713 
    714         :raises InvalidSpecifier:
    715             If the given ``specifiers`` are not parseable than this exception will be
    716             raised.
    717         """
    718 
    719         if isinstance(specifiers, str):
    720             # Split on `,` to break each individual specifier into its own item, and
    721             # strip each item to remove leading/trailing whitespace.
    722             split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
    723 
    724             # Make each individual specifier a Specifier and save in a frozen set
    725             # for later.
    726             self._specs = frozenset(map(Specifier, split_specifiers))
    727         else:
    728             # Save the supplied specifiers in a frozen set.
    729             self._specs = frozenset(specifiers)
    730 
    731         # Store our prereleases value so we can use it later to determine if
    732         # we accept prereleases or not.
    733         self._prereleases = prereleases
    734 
    735     @property
    736     def prereleases(self) -> bool | None:
    737         # If we have been given an explicit prerelease modifier, then we'll
    738         # pass that through here.
    739         if self._prereleases is not None:
    740             return self._prereleases
    741 
    742         # If we don't have any specifiers, and we don't have a forced value,
    743         # then we'll just return None since we don't know if this should have
    744         # pre-releases or not.
    745         if not self._specs:
    746             return None
    747 
    748         # Otherwise we'll see if any of the given specifiers accept
    749         # prereleases, if any of them do we'll return True, otherwise False.
    750         return any(s.prereleases for s in self._specs)
    751 
    752     @prereleases.setter
    753     def prereleases(self, value: bool) -> None:
    754         self._prereleases = value
    755 
    756     def __repr__(self) -> str:
    757         """A representation of the specifier set that shows all internal state.
    758 
    759         Note that the ordering of the individual specifiers within the set may not
    760         match the input string.
    761 
    762         >>> SpecifierSet('>=1.0.0,!=2.0.0')
    763         <SpecifierSet('!=2.0.0,>=1.0.0')>
    764         >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False)
    765         <SpecifierSet('!=2.0.0,>=1.0.0', prereleases=False)>
    766         >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True)
    767         <SpecifierSet('!=2.0.0,>=1.0.0', prereleases=True)>
    768         """
    769         pre = (
    770             f", prereleases={self.prereleases!r}"
    771             if self._prereleases is not None
    772             else ""
    773         )
    774 
    775         return f"<SpecifierSet({str(self)!r}{pre})>"
    776 
    777     def __str__(self) -> str:
    778         """A string representation of the specifier set that can be round-tripped.
    779 
    780         Note that the ordering of the individual specifiers within the set may not
    781         match the input string.
    782 
    783         >>> str(SpecifierSet(">=1.0.0,!=1.0.1"))
    784         '!=1.0.1,>=1.0.0'
    785         >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False))
    786         '!=1.0.1,>=1.0.0'
    787         """
    788         return ",".join(sorted(str(s) for s in self._specs))
    789 
    790     def __hash__(self) -> int:
    791         return hash(self._specs)
    792 
    793     def __and__(self, other: SpecifierSet | str) -> SpecifierSet:
    794         """Return a SpecifierSet which is a combination of the two sets.
    795 
    796         :param other: The other object to combine with.
    797 
    798         >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1'
    799         <SpecifierSet('!=1.0.1,!=2.0.1,<=2.0.0,>=1.0.0')>
    800         >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1')
    801         <SpecifierSet('!=1.0.1,!=2.0.1,<=2.0.0,>=1.0.0')>
    802         """
    803         if isinstance(other, str):
    804             other = SpecifierSet(other)
    805         elif not isinstance(other, SpecifierSet):
    806             return NotImplemented
    807 
    808         specifier = SpecifierSet()
    809         specifier._specs = frozenset(self._specs | other._specs)
    810 
    811         if self._prereleases is None and other._prereleases is not None:
    812             specifier._prereleases = other._prereleases
    813         elif self._prereleases is not None and other._prereleases is None:
    814             specifier._prereleases = self._prereleases
    815         elif self._prereleases == other._prereleases:
    816             specifier._prereleases = self._prereleases
    817         else:
    818             raise ValueError(
    819                 "Cannot combine SpecifierSets with True and False prerelease "
    820                 "overrides."
    821             )
    822 
    823         return specifier
    824 
    825     def __eq__(self, other: object) -> bool:
    826         """Whether or not the two SpecifierSet-like objects are equal.
    827 
    828         :param other: The other object to check against.
    829 
    830         The value of :attr:`prereleases` is ignored.
    831 
    832         >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1")
    833         True
    834         >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) ==
    835         ...  SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True))
    836         True
    837         >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1"
    838         True
    839         >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0")
    840         False
    841         >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2")
    842         False
    843         """
    844         if isinstance(other, (str, Specifier)):
    845             other = SpecifierSet(str(other))
    846         elif not isinstance(other, SpecifierSet):
    847             return NotImplemented
    848 
    849         return self._specs == other._specs
    850 
    851     def __len__(self) -> int:
    852         """Returns the number of specifiers in this specifier set."""
    853         return len(self._specs)
    854 
    855     def __iter__(self) -> Iterator[Specifier]:
    856         """
    857         Returns an iterator over all the underlying :class:`Specifier` instances
    858         in this specifier set.
    859 
    860         >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str)
    861         [<Specifier('!=1.0.1')>, <Specifier('>=1.0.0')>]
    862         """
    863         return iter(self._specs)
    864 
    865     def __contains__(self, item: UnparsedVersion) -> bool:
    866         """Return whether or not the item is contained in this specifier.
    867 
    868         :param item: The item to check for.
    869 
    870         This is used for the ``in`` operator and behaves the same as
    871         :meth:`contains` with no ``prereleases`` argument passed.
    872 
    873         >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1")
    874         True
    875         >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1")
    876         True
    877         >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1")
    878         False
    879         >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1")
    880         False
    881         >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)
    882         True
    883         """
    884         return self.contains(item)
    885 
    886     def contains(
    887         self,
    888         item: UnparsedVersion,
    889         prereleases: bool | None = None,
    890         installed: bool | None = None,
    891     ) -> bool:
    892         """Return whether or not the item is contained in this SpecifierSet.
    893 
    894         :param item:
    895             The item to check for, which can be a version string or a
    896             :class:`Version` instance.
    897         :param prereleases:
    898             Whether or not to match prereleases with this SpecifierSet. If set to
    899             ``None`` (the default), it uses :attr:`prereleases` to determine
    900             whether or not prereleases are allowed.
    901 
    902         >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3")
    903         True
    904         >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3"))
    905         True
    906         >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1")
    907         False
    908         >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1")
    909         False
    910         >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1")
    911         True
    912         >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True)
    913         True
    914         """
    915         # Ensure that our item is a Version instance.
    916         if not isinstance(item, Version):
    917             item = Version(item)
    918 
    919         # Determine if we're forcing a prerelease or not, if we're not forcing
    920         # one for this particular filter call, then we'll use whatever the
    921         # SpecifierSet thinks for whether or not we should support prereleases.
    922         if prereleases is None:
    923             prereleases = self.prereleases
    924 
    925         # We can determine if we're going to allow pre-releases by looking to
    926         # see if any of the underlying items supports them. If none of them do
    927         # and this item is a pre-release then we do not allow it and we can
    928         # short circuit that here.
    929         # Note: This means that 1.0.dev1 would not be contained in something
    930         #       like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0
    931         if not prereleases and item.is_prerelease:
    932             return False
    933 
    934         if installed and item.is_prerelease:
    935             item = Version(item.base_version)
    936 
    937         # We simply dispatch to the underlying specs here to make sure that the
    938         # given version is contained within all of them.
    939         # Note: This use of all() here means that an empty set of specifiers
    940         #       will always return True, this is an explicit design decision.
    941         return all(s.contains(item, prereleases=prereleases) for s in self._specs)
    942 
    943     def filter(
    944         self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
    945     ) -> Iterator[UnparsedVersionVar]:
    946         """Filter items in the given iterable, that match the specifiers in this set.
    947 
    948         :param iterable:
    949             An iterable that can contain version strings and :class:`Version` instances.
    950             The items in the iterable will be filtered according to the specifier.
    951         :param prereleases:
    952             Whether or not to allow prereleases in the returned iterator. If set to
    953             ``None`` (the default), it will be intelligently decide whether to allow
    954             prereleases or not (based on the :attr:`prereleases` attribute, and
    955             whether the only versions matching are prereleases).
    956 
    957         This method is smarter than just ``filter(SpecifierSet(...).contains, [...])``
    958         because it implements the rule from :pep:`440` that a prerelease item
    959         SHOULD be accepted if no other versions match the given specifier.
    960 
    961         >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"]))
    962         ['1.3']
    963         >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")]))
    964         ['1.3', <Version('1.4')>]
    965         >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"]))
    966         []
    967         >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True))
    968         ['1.3', '1.5a1']
    969         >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"]))
    970         ['1.3', '1.5a1']
    971 
    972         An "empty" SpecifierSet will filter items based on the presence of prerelease
    973         versions in the set.
    974 
    975         >>> list(SpecifierSet("").filter(["1.3", "1.5a1"]))
    976         ['1.3']
    977         >>> list(SpecifierSet("").filter(["1.5a1"]))
    978         ['1.5a1']
    979         >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"]))
    980         ['1.3', '1.5a1']
    981         >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True))
    982         ['1.3', '1.5a1']
    983         """
    984         # Determine if we're forcing a prerelease or not, if we're not forcing
    985         # one for this particular filter call, then we'll use whatever the
    986         # SpecifierSet thinks for whether or not we should support prereleases.
    987         if prereleases is None:
    988             prereleases = self.prereleases
    989 
    990         # If we have any specifiers, then we want to wrap our iterable in the
    991         # filter method for each one, this will act as a logical AND amongst
    992         # each specifier.
    993         if self._specs:
    994             for spec in self._specs:
    995                 iterable = spec.filter(iterable, prereleases=bool(prereleases))
    996             return iter(iterable)
    997         # If we do not have any specifiers, then we need to have a rough filter
    998         # which will filter out any pre-releases, unless there are no final
    999         # releases.
   1000         else:
   1001             filtered: list[UnparsedVersionVar] = []
   1002             found_prereleases: list[UnparsedVersionVar] = []
   1003 
   1004             for item in iterable:
   1005                 parsed_version = _coerce_version(item)
   1006 
   1007                 # Store any item which is a pre-release for later unless we've
   1008                 # already found a final version or we are accepting prereleases
   1009                 if parsed_version.is_prerelease and not prereleases:
   1010                     if not filtered:
   1011                         found_prereleases.append(item)
   1012                 else:
   1013                     filtered.append(item)
   1014 
   1015             # If we've found no items except for pre-releases, then we'll go
   1016             # ahead and use the pre-releases
   1017             if not filtered and found_prereleases and prereleases is None:
   1018                 return iter(found_prereleases)
   1019 
   1020             return iter(filtered)