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 5import abc 6import functools 7import itertools 8import re 9import warnings 10from typing import ( 11 Callable, 12 Dict, 13 Iterable, 14 Iterator, 15 List, 16 Optional, 17 Pattern, 18 Set, 19 Tuple, 20 TypeVar, 21 Union, 22) 23 24from .utils import canonicalize_version 25from .version import LegacyVersion, Version, parse 26 27ParsedVersion = Union[Version, LegacyVersion] 28UnparsedVersion = Union[Version, LegacyVersion, str] 29VersionTypeVar = TypeVar("VersionTypeVar", bound=UnparsedVersion) 30CallableOperator = Callable[[ParsedVersion, str], bool] 31 32 33class InvalidSpecifier(ValueError): 34 """ 35 An invalid specifier was found, users should refer to PEP 440. 36 """ 37 38 39class BaseSpecifier(metaclass=abc.ABCMeta): 40 @abc.abstractmethod 41 def __str__(self) -> str: 42 """ 43 Returns the str representation of this Specifier like object. This 44 should be representative of the Specifier itself. 45 """ 46 47 @abc.abstractmethod 48 def __hash__(self) -> int: 49 """ 50 Returns a hash value for this Specifier like object. 51 """ 52 53 @abc.abstractmethod 54 def __eq__(self, other: object) -> bool: 55 """ 56 Returns a boolean representing whether or not the two Specifier like 57 objects are equal. 58 """ 59 60 @abc.abstractproperty 61 def prereleases(self) -> Optional[bool]: 62 """ 63 Returns whether or not pre-releases as a whole are allowed by this 64 specifier. 65 """ 66 67 @prereleases.setter 68 def prereleases(self, value: bool) -> None: 69 """ 70 Sets whether or not pre-releases as a whole are allowed by this 71 specifier. 72 """ 73 74 @abc.abstractmethod 75 def contains(self, item: str, prereleases: Optional[bool] = None) -> bool: 76 """ 77 Determines if the given item is contained within this specifier. 78 """ 79 80 @abc.abstractmethod 81 def filter( 82 self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None 83 ) -> Iterable[VersionTypeVar]: 84 """ 85 Takes an iterable of items and filters them so that only items which 86 are contained within this specifier are allowed in it. 87 """ 88 89 90class _IndividualSpecifier(BaseSpecifier): 91 92 _operators: Dict[str, str] = {} 93 _regex: Pattern[str] 94 95 def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: 96 match = self._regex.search(spec) 97 if not match: 98 raise InvalidSpecifier(f"Invalid specifier: '{spec}'") 99 100 self._spec: Tuple[str, str] = ( 101 match.group("operator").strip(), 102 match.group("version").strip(), 103 ) 104 105 # Store whether or not this Specifier should accept prereleases 106 self._prereleases = prereleases 107 108 def __repr__(self) -> str: 109 pre = ( 110 f", prereleases={self.prereleases!r}" 111 if self._prereleases is not None 112 else "" 113 ) 114 115 return f"<{self.__class__.__name__}({str(self)!r}{pre})>" 116 117 def __str__(self) -> str: 118 return "{}{}".format(*self._spec) 119 120 @property 121 def _canonical_spec(self) -> Tuple[str, str]: 122 return self._spec[0], canonicalize_version(self._spec[1]) 123 124 def __hash__(self) -> int: 125 return hash(self._canonical_spec) 126 127 def __eq__(self, other: object) -> bool: 128 if isinstance(other, str): 129 try: 130 other = self.__class__(str(other)) 131 except InvalidSpecifier: 132 return NotImplemented 133 elif not isinstance(other, self.__class__): 134 return NotImplemented 135 136 return self._canonical_spec == other._canonical_spec 137 138 def _get_operator(self, op: str) -> CallableOperator: 139 operator_callable: CallableOperator = getattr( 140 self, f"_compare_{self._operators[op]}" 141 ) 142 return operator_callable 143 144 def _coerce_version(self, version: UnparsedVersion) -> ParsedVersion: 145 if not isinstance(version, (LegacyVersion, Version)): 146 version = parse(version) 147 return version 148 149 @property 150 def operator(self) -> str: 151 return self._spec[0] 152 153 @property 154 def version(self) -> str: 155 return self._spec[1] 156 157 @property 158 def prereleases(self) -> Optional[bool]: 159 return self._prereleases 160 161 @prereleases.setter 162 def prereleases(self, value: bool) -> None: 163 self._prereleases = value 164 165 def __contains__(self, item: str) -> bool: 166 return self.contains(item) 167 168 def contains( 169 self, item: UnparsedVersion, prereleases: Optional[bool] = None 170 ) -> bool: 171 172 # Determine if prereleases are to be allowed or not. 173 if prereleases is None: 174 prereleases = self.prereleases 175 176 # Normalize item to a Version or LegacyVersion, this allows us to have 177 # a shortcut for ``"2.0" in Specifier(">=2") 178 normalized_item = self._coerce_version(item) 179 180 # Determine if we should be supporting prereleases in this specifier 181 # or not, if we do not support prereleases than we can short circuit 182 # logic if this version is a prereleases. 183 if normalized_item.is_prerelease and not prereleases: 184 return False 185 186 # Actually do the comparison to determine if this item is contained 187 # within this Specifier or not. 188 operator_callable: CallableOperator = self._get_operator(self.operator) 189 return operator_callable(normalized_item, self.version) 190 191 def filter( 192 self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None 193 ) -> Iterable[VersionTypeVar]: 194 195 yielded = False 196 found_prereleases = [] 197 198 kw = {"prereleases": prereleases if prereleases is not None else True} 199 200 # Attempt to iterate over all the values in the iterable and if any of 201 # them match, yield them. 202 for version in iterable: 203 parsed_version = self._coerce_version(version) 204 205 if self.contains(parsed_version, **kw): 206 # If our version is a prerelease, and we were not set to allow 207 # prereleases, then we'll store it for later in case nothing 208 # else matches this specifier. 209 if parsed_version.is_prerelease and not ( 210 prereleases or self.prereleases 211 ): 212 found_prereleases.append(version) 213 # Either this is not a prerelease, or we should have been 214 # accepting prereleases from the beginning. 215 else: 216 yielded = True 217 yield version 218 219 # Now that we've iterated over everything, determine if we've yielded 220 # any values, and if we have not and we have any prereleases stored up 221 # then we will go ahead and yield the prereleases. 222 if not yielded and found_prereleases: 223 for version in found_prereleases: 224 yield version 225 226 227class LegacySpecifier(_IndividualSpecifier): 228 229 _regex_str = r""" 230 (?P<operator>(==|!=|<=|>=|<|>)) 231 \s* 232 (?P<version> 233 [^,;\s)]* # Since this is a "legacy" specifier, and the version 234 # string can be just about anything, we match everything 235 # except for whitespace, a semi-colon for marker support, 236 # a closing paren since versions can be enclosed in 237 # them, and a comma since it's a version separator. 238 ) 239 """ 240 241 _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) 242 243 _operators = { 244 "==": "equal", 245 "!=": "not_equal", 246 "<=": "less_than_equal", 247 ">=": "greater_than_equal", 248 "<": "less_than", 249 ">": "greater_than", 250 } 251 252 def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: 253 super().__init__(spec, prereleases) 254 255 warnings.warn( 256 "Creating a LegacyVersion has been deprecated and will be " 257 "removed in the next major release", 258 DeprecationWarning, 259 ) 260 261 def _coerce_version(self, version: UnparsedVersion) -> LegacyVersion: 262 if not isinstance(version, LegacyVersion): 263 version = LegacyVersion(str(version)) 264 return version 265 266 def _compare_equal(self, prospective: LegacyVersion, spec: str) -> bool: 267 return prospective == self._coerce_version(spec) 268 269 def _compare_not_equal(self, prospective: LegacyVersion, spec: str) -> bool: 270 return prospective != self._coerce_version(spec) 271 272 def _compare_less_than_equal(self, prospective: LegacyVersion, spec: str) -> bool: 273 return prospective <= self._coerce_version(spec) 274 275 def _compare_greater_than_equal( 276 self, prospective: LegacyVersion, spec: str 277 ) -> bool: 278 return prospective >= self._coerce_version(spec) 279 280 def _compare_less_than(self, prospective: LegacyVersion, spec: str) -> bool: 281 return prospective < self._coerce_version(spec) 282 283 def _compare_greater_than(self, prospective: LegacyVersion, spec: str) -> bool: 284 return prospective > self._coerce_version(spec) 285 286 287def _require_version_compare( 288 fn: Callable[["Specifier", ParsedVersion, str], bool] 289) -> Callable[["Specifier", ParsedVersion, str], bool]: 290 @functools.wraps(fn) 291 def wrapped(self: "Specifier", prospective: ParsedVersion, spec: str) -> bool: 292 if not isinstance(prospective, Version): 293 return False 294 return fn(self, prospective, spec) 295 296 return wrapped 297 298 299class Specifier(_IndividualSpecifier): 300 301 _regex_str = r""" 302 (?P<operator>(~=|==|!=|<=|>=|<|>|===)) 303 (?P<version> 304 (?: 305 # The identity operators allow for an escape hatch that will 306 # do an exact string match of the version you wish to install. 307 # This will not be parsed by PEP 440 and we cannot determine 308 # any semantic meaning from it. This operator is discouraged 309 # but included entirely as an escape hatch. 310 (?<====) # Only match for the identity operator 311 \s* 312 [^\s]* # We just match everything, except for whitespace 313 # since we are only testing for strict identity. 314 ) 315 | 316 (?: 317 # The (non)equality operators allow for wild card and local 318 # versions to be specified so we have to define these two 319 # operators separately to enable that. 320 (?<===|!=) # Only match for equals and not equals 321 322 \s* 323 v? 324 (?:[0-9]+!)? # epoch 325 [0-9]+(?:\.[0-9]+)* # release 326 (?: # pre release 327 [-_\.]? 328 (a|b|c|rc|alpha|beta|pre|preview) 329 [-_\.]? 330 [0-9]* 331 )? 332 (?: # post release 333 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) 334 )? 335 336 # You cannot use a wild card and a dev or local version 337 # together so group them with a | and make them optional. 338 (?: 339 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release 340 (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local 341 | 342 \.\* # Wild card syntax of .* 343 )? 344 ) 345 | 346 (?: 347 # The compatible operator requires at least two digits in the 348 # release segment. 349 (?<=~=) # Only match for the compatible operator 350 351 \s* 352 v? 353 (?:[0-9]+!)? # epoch 354 [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *) 355 (?: # pre release 356 [-_\.]? 357 (a|b|c|rc|alpha|beta|pre|preview) 358 [-_\.]? 359 [0-9]* 360 )? 361 (?: # post release 362 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) 363 )? 364 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release 365 ) 366 | 367 (?: 368 # All other operators only allow a sub set of what the 369 # (non)equality operators do. Specifically they do not allow 370 # local versions to be specified nor do they allow the prefix 371 # matching wild cards. 372 (?<!==|!=|~=) # We have special cases for these 373 # operators so we want to make sure they 374 # don't match here. 375 376 \s* 377 v? 378 (?:[0-9]+!)? # epoch 379 [0-9]+(?:\.[0-9]+)* # release 380 (?: # pre release 381 [-_\.]? 382 (a|b|c|rc|alpha|beta|pre|preview) 383 [-_\.]? 384 [0-9]* 385 )? 386 (?: # post release 387 (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) 388 )? 389 (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release 390 ) 391 ) 392 """ 393 394 _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) 395 396 _operators = { 397 "~=": "compatible", 398 "==": "equal", 399 "!=": "not_equal", 400 "<=": "less_than_equal", 401 ">=": "greater_than_equal", 402 "<": "less_than", 403 ">": "greater_than", 404 "===": "arbitrary", 405 } 406 407 @_require_version_compare 408 def _compare_compatible(self, prospective: ParsedVersion, spec: str) -> bool: 409 410 # Compatible releases have an equivalent combination of >= and ==. That 411 # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to 412 # implement this in terms of the other specifiers instead of 413 # implementing it ourselves. The only thing we need to do is construct 414 # the other specifiers. 415 416 # We want everything but the last item in the version, but we want to 417 # ignore suffix segments. 418 prefix = ".".join( 419 list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] 420 ) 421 422 # Add the prefix notation to the end of our string 423 prefix += ".*" 424 425 return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( 426 prospective, prefix 427 ) 428 429 @_require_version_compare 430 def _compare_equal(self, prospective: ParsedVersion, spec: str) -> bool: 431 432 # We need special logic to handle prefix matching 433 if spec.endswith(".*"): 434 # In the case of prefix matching we want to ignore local segment. 435 prospective = Version(prospective.public) 436 # Split the spec out by dots, and pretend that there is an implicit 437 # dot in between a release segment and a pre-release segment. 438 split_spec = _version_split(spec[:-2]) # Remove the trailing .* 439 440 # Split the prospective version out by dots, and pretend that there 441 # is an implicit dot in between a release segment and a pre-release 442 # segment. 443 split_prospective = _version_split(str(prospective)) 444 445 # Shorten the prospective version to be the same length as the spec 446 # so that we can determine if the specifier is a prefix of the 447 # prospective version or not. 448 shortened_prospective = split_prospective[: len(split_spec)] 449 450 # Pad out our two sides with zeros so that they both equal the same 451 # length. 452 padded_spec, padded_prospective = _pad_version( 453 split_spec, shortened_prospective 454 ) 455 456 return padded_prospective == padded_spec 457 else: 458 # Convert our spec string into a Version 459 spec_version = Version(spec) 460 461 # If the specifier does not have a local segment, then we want to 462 # act as if the prospective version also does not have a local 463 # segment. 464 if not spec_version.local: 465 prospective = Version(prospective.public) 466 467 return prospective == spec_version 468 469 @_require_version_compare 470 def _compare_not_equal(self, prospective: ParsedVersion, spec: str) -> bool: 471 return not self._compare_equal(prospective, spec) 472 473 @_require_version_compare 474 def _compare_less_than_equal(self, prospective: ParsedVersion, spec: str) -> bool: 475 476 # NB: Local version identifiers are NOT permitted in the version 477 # specifier, so local version labels can be universally removed from 478 # the prospective version. 479 return Version(prospective.public) <= Version(spec) 480 481 @_require_version_compare 482 def _compare_greater_than_equal( 483 self, prospective: ParsedVersion, spec: str 484 ) -> bool: 485 486 # NB: Local version identifiers are NOT permitted in the version 487 # specifier, so local version labels can be universally removed from 488 # the prospective version. 489 return Version(prospective.public) >= Version(spec) 490 491 @_require_version_compare 492 def _compare_less_than(self, prospective: ParsedVersion, spec_str: str) -> bool: 493 494 # Convert our spec to a Version instance, since we'll want to work with 495 # it as a version. 496 spec = Version(spec_str) 497 498 # Check to see if the prospective version is less than the spec 499 # version. If it's not we can short circuit and just return False now 500 # instead of doing extra unneeded work. 501 if not prospective < spec: 502 return False 503 504 # This special case is here so that, unless the specifier itself 505 # includes is a pre-release version, that we do not accept pre-release 506 # versions for the version mentioned in the specifier (e.g. <3.1 should 507 # not match 3.1.dev0, but should match 3.0.dev0). 508 if not spec.is_prerelease and prospective.is_prerelease: 509 if Version(prospective.base_version) == Version(spec.base_version): 510 return False 511 512 # If we've gotten to here, it means that prospective version is both 513 # less than the spec version *and* it's not a pre-release of the same 514 # version in the spec. 515 return True 516 517 @_require_version_compare 518 def _compare_greater_than(self, prospective: ParsedVersion, spec_str: str) -> bool: 519 520 # Convert our spec to a Version instance, since we'll want to work with 521 # it as a version. 522 spec = Version(spec_str) 523 524 # Check to see if the prospective version is greater than the spec 525 # version. If it's not we can short circuit and just return False now 526 # instead of doing extra unneeded work. 527 if not prospective > spec: 528 return False 529 530 # This special case is here so that, unless the specifier itself 531 # includes is a post-release version, that we do not accept 532 # post-release versions for the version mentioned in the specifier 533 # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). 534 if not spec.is_postrelease and prospective.is_postrelease: 535 if Version(prospective.base_version) == Version(spec.base_version): 536 return False 537 538 # Ensure that we do not allow a local version of the version mentioned 539 # in the specifier, which is technically greater than, to match. 540 if prospective.local is not None: 541 if Version(prospective.base_version) == Version(spec.base_version): 542 return False 543 544 # If we've gotten to here, it means that prospective version is both 545 # greater than the spec version *and* it's not a pre-release of the 546 # same version in the spec. 547 return True 548 549 def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: 550 return str(prospective).lower() == str(spec).lower() 551 552 @property 553 def prereleases(self) -> bool: 554 555 # If there is an explicit prereleases set for this, then we'll just 556 # blindly use that. 557 if self._prereleases is not None: 558 return self._prereleases 559 560 # Look at all of our specifiers and determine if they are inclusive 561 # operators, and if they are if they are including an explicit 562 # prerelease. 563 operator, version = self._spec 564 if operator in ["==", ">=", "<=", "~=", "==="]: 565 # The == specifier can include a trailing .*, if it does we 566 # want to remove before parsing. 567 if operator == "==" and version.endswith(".*"): 568 version = version[:-2] 569 570 # Parse the version, and if it is a pre-release than this 571 # specifier allows pre-releases. 572 if parse(version).is_prerelease: 573 return True 574 575 return False 576 577 @prereleases.setter 578 def prereleases(self, value: bool) -> None: 579 self._prereleases = value 580 581 582_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") 583 584 585def _version_split(version: str) -> List[str]: 586 result: List[str] = [] 587 for item in version.split("."): 588 match = _prefix_regex.search(item) 589 if match: 590 result.extend(match.groups()) 591 else: 592 result.append(item) 593 return result 594 595 596def _is_not_suffix(segment: str) -> bool: 597 return not any( 598 segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") 599 ) 600 601 602def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]: 603 left_split, right_split = [], [] 604 605 # Get the release segment of our versions 606 left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left))) 607 right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) 608 609 # Get the rest of our versions 610 left_split.append(left[len(left_split[0]) :]) 611 right_split.append(right[len(right_split[0]) :]) 612 613 # Insert our padding 614 left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) 615 right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) 616 617 return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split))) 618 619 620class SpecifierSet(BaseSpecifier): 621 def __init__( 622 self, specifiers: str = "", prereleases: Optional[bool] = None 623 ) -> None: 624 625 # Split on , to break each individual specifier into it's own item, and 626 # strip each item to remove leading/trailing whitespace. 627 split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] 628 629 # Parsed each individual specifier, attempting first to make it a 630 # Specifier and falling back to a LegacySpecifier. 631 parsed: Set[_IndividualSpecifier] = set() 632 for specifier in split_specifiers: 633 try: 634 parsed.add(Specifier(specifier)) 635 except InvalidSpecifier: 636 parsed.add(LegacySpecifier(specifier)) 637 638 # Turn our parsed specifiers into a frozen set and save them for later. 639 self._specs = frozenset(parsed) 640 641 # Store our prereleases value so we can use it later to determine if 642 # we accept prereleases or not. 643 self._prereleases = prereleases 644 645 def __repr__(self) -> str: 646 pre = ( 647 f", prereleases={self.prereleases!r}" 648 if self._prereleases is not None 649 else "" 650 ) 651 652 return f"<SpecifierSet({str(self)!r}{pre})>" 653 654 def __str__(self) -> str: 655 return ",".join(sorted(str(s) for s in self._specs)) 656 657 def __hash__(self) -> int: 658 return hash(self._specs) 659 660 def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet": 661 if isinstance(other, str): 662 other = SpecifierSet(other) 663 elif not isinstance(other, SpecifierSet): 664 return NotImplemented 665 666 specifier = SpecifierSet() 667 specifier._specs = frozenset(self._specs | other._specs) 668 669 if self._prereleases is None and other._prereleases is not None: 670 specifier._prereleases = other._prereleases 671 elif self._prereleases is not None and other._prereleases is None: 672 specifier._prereleases = self._prereleases 673 elif self._prereleases == other._prereleases: 674 specifier._prereleases = self._prereleases 675 else: 676 raise ValueError( 677 "Cannot combine SpecifierSets with True and False prerelease " 678 "overrides." 679 ) 680 681 return specifier 682 683 def __eq__(self, other: object) -> bool: 684 if isinstance(other, (str, _IndividualSpecifier)): 685 other = SpecifierSet(str(other)) 686 elif not isinstance(other, SpecifierSet): 687 return NotImplemented 688 689 return self._specs == other._specs 690 691 def __len__(self) -> int: 692 return len(self._specs) 693 694 def __iter__(self) -> Iterator[_IndividualSpecifier]: 695 return iter(self._specs) 696 697 @property 698 def prereleases(self) -> Optional[bool]: 699 700 # If we have been given an explicit prerelease modifier, then we'll 701 # pass that through here. 702 if self._prereleases is not None: 703 return self._prereleases 704 705 # If we don't have any specifiers, and we don't have a forced value, 706 # then we'll just return None since we don't know if this should have 707 # pre-releases or not. 708 if not self._specs: 709 return None 710 711 # Otherwise we'll see if any of the given specifiers accept 712 # prereleases, if any of them do we'll return True, otherwise False. 713 return any(s.prereleases for s in self._specs) 714 715 @prereleases.setter 716 def prereleases(self, value: bool) -> None: 717 self._prereleases = value 718 719 def __contains__(self, item: UnparsedVersion) -> bool: 720 return self.contains(item) 721 722 def contains( 723 self, item: UnparsedVersion, prereleases: Optional[bool] = None 724 ) -> bool: 725 726 # Ensure that our item is a Version or LegacyVersion instance. 727 if not isinstance(item, (LegacyVersion, Version)): 728 item = parse(item) 729 730 # Determine if we're forcing a prerelease or not, if we're not forcing 731 # one for this particular filter call, then we'll use whatever the 732 # SpecifierSet thinks for whether or not we should support prereleases. 733 if prereleases is None: 734 prereleases = self.prereleases 735 736 # We can determine if we're going to allow pre-releases by looking to 737 # see if any of the underlying items supports them. If none of them do 738 # and this item is a pre-release then we do not allow it and we can 739 # short circuit that here. 740 # Note: This means that 1.0.dev1 would not be contained in something 741 # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 742 if not prereleases and item.is_prerelease: 743 return False 744 745 # We simply dispatch to the underlying specs here to make sure that the 746 # given version is contained within all of them. 747 # Note: This use of all() here means that an empty set of specifiers 748 # will always return True, this is an explicit design decision. 749 return all(s.contains(item, prereleases=prereleases) for s in self._specs) 750 751 def filter( 752 self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None 753 ) -> Iterable[VersionTypeVar]: 754 755 # Determine if we're forcing a prerelease or not, if we're not forcing 756 # one for this particular filter call, then we'll use whatever the 757 # SpecifierSet thinks for whether or not we should support prereleases. 758 if prereleases is None: 759 prereleases = self.prereleases 760 761 # If we have any specifiers, then we want to wrap our iterable in the 762 # filter method for each one, this will act as a logical AND amongst 763 # each specifier. 764 if self._specs: 765 for spec in self._specs: 766 iterable = spec.filter(iterable, prereleases=bool(prereleases)) 767 return iterable 768 # If we do not have any specifiers, then we need to have a rough filter 769 # which will filter out any pre-releases, unless there are no final 770 # releases, and which will filter out LegacyVersion in general. 771 else: 772 filtered: List[VersionTypeVar] = [] 773 found_prereleases: List[VersionTypeVar] = [] 774 775 item: UnparsedVersion 776 parsed_version: Union[Version, LegacyVersion] 777 778 for item in iterable: 779 # Ensure that we some kind of Version class for this item. 780 if not isinstance(item, (LegacyVersion, Version)): 781 parsed_version = parse(item) 782 else: 783 parsed_version = item 784 785 # Filter out any item which is parsed as a LegacyVersion 786 if isinstance(parsed_version, LegacyVersion): 787 continue 788 789 # Store any item which is a pre-release for later unless we've 790 # already found a final version or we are accepting prereleases 791 if parsed_version.is_prerelease and not prereleases: 792 if not filtered: 793 found_prereleases.append(item) 794 else: 795 filtered.append(item) 796 797 # If we've found no items except for pre-releases, then we'll go 798 # ahead and use the pre-releases 799 if not filtered and found_prereleases and prereleases is None: 800 return found_prereleases 801 802 return filtered 803