]>
Commit | Line | Data |
---|---|---|
e0df8241 JR |
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 | import abc | |
6 | import functools | |
7 | import itertools | |
8 | import re | |
9 | import warnings | |
10 | from 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 | ||
24 | from .utils import canonicalize_version | |
25 | from .version import LegacyVersion, Version, parse | |
26 | ||
27 | ParsedVersion = Union[Version, LegacyVersion] | |
28 | UnparsedVersion = Union[Version, LegacyVersion, str] | |
29 | VersionTypeVar = TypeVar("VersionTypeVar", bound=UnparsedVersion) | |
30 | CallableOperator = Callable[[ParsedVersion, str], bool] | |
31 | ||
32 | ||
33 | class InvalidSpecifier(ValueError): | |
34 | """ | |
35 | An invalid specifier was found, users should refer to PEP 440. | |
36 | """ | |
37 | ||
38 | ||
39 | class 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 | ||
90 | class _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 | ||
227 | class 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 | ||
287 | def _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 | ||
299 | class 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 | ||
585 | def _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 | ||
596 | def _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 | ||
602 | def _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 | ||
620 | class 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 |