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