annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/packaging/specifiers.py @ 68:5028fdace37b

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