Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/_ranges.py: 24%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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"""Private version-range helpers used by :mod:`packaging.specifiers`."""
6from __future__ import annotations
8import enum
9import functools
10from typing import (
11 TYPE_CHECKING,
12 Any,
13 Final,
14)
16from .version import InvalidVersion, Version
18if TYPE_CHECKING:
19 from collections.abc import Callable, Iterable, Iterator, Sequence
20 from typing import Union
22 # Total-order key for comparing two boundaries (boundary-vs-boundary only).
23 # The post slot may be ``_BOUNDARY_INF`` for an AFTER_POSTS boundary.
24 _BoundaryOrderSuffix = tuple[int, int, int, Union[int, float], int, int]
25 _BoundaryOrderKey = tuple[int, tuple[int, ...], _BoundaryOrderSuffix, float]
27__all__ = [
28 "FULL_RANGE",
29 "bounds_for_spec",
30 "coerce_version",
31 "filter_by_ranges",
32 "intersect_ranges",
33 "intersect_specifier_bounds",
34 "least_version_above",
35 "matches_bounds_only",
36 "range_is_empty",
37 "ranges_are_prerelease_only",
38 "resolve_prereleases",
39 "standard_ranges",
40 "wildcard_ranges",
41]
43#: The smallest possible PEP 440 version. No valid version is less than this.
44MIN_VERSION: Final[Version] = Version("0.dev0")
46#: The smallest non-pre-release version, i.e. the nearest non-pre-release at or
47#: above the ``-inf`` floor.
48MIN_RELEASE: Final[Version] = Version("0")
50#: Sorts above any real post number and any local label, so a boundary can be
51#: ordered above the version family it covers when two boundaries are compared.
52_BOUNDARY_INF: Final[float] = float("inf")
55class BoundaryKind(enum.Enum):
56 """Where a boundary marker sits in the version ordering."""
58 AFTER_LOCALS = enum.auto() # after V+local, before V.post0
59 AFTER_POSTS = enum.auto() # after V.postN, before next release
62@functools.total_ordering
63class BoundaryVersion:
64 """A point on the version line between two real PEP 440 versions.
66 Relative to a base version V::
68 V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V)
70 AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no
71 local), and the lower bound of the upper-side range of ``!=V``.
72 AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release),
73 excluding V's post-releases per PEP 440.
74 """
76 __slots__ = (
77 "_cached_dev",
78 "_cached_epoch",
79 "_cached_post",
80 "_cached_pre",
81 "_cached_trimmed_release",
82 "kind",
83 "version",
84 )
86 def __init__(self, version: Version, kind: BoundaryKind) -> None:
87 self.version = version
88 self.kind = kind
89 self._cached_trimmed_release = trim_release(version.release)
90 self._cached_epoch = version.epoch
91 self._cached_pre = version.pre
92 self._cached_post = version.post
93 self._cached_dev = version.dev
95 def _is_family(self, other: Version) -> bool:
96 """Is ``other`` a version that this boundary sorts above?"""
97 if other.epoch != self._cached_epoch:
98 return False
99 # Inline release-trim comparison: other.release matches the
100 # trimmed release iff its leading slice is equal and any extra
101 # components are zero. Avoids trim_release's tuple allocation.
102 other_release = other.release
103 trimmed_release = self._cached_trimmed_release
104 trimmed_length = len(trimmed_release)
105 if len(other_release) < trimmed_length:
106 return False
107 if other_release[:trimmed_length] != trimmed_release:
108 return False
109 for i in range(trimmed_length, len(other_release)):
110 if other_release[i] != 0:
111 return False
112 if other.pre != self._cached_pre:
113 return False
114 if self.kind == BoundaryKind.AFTER_LOCALS:
115 # Local family: same public version, any local label.
116 return other.post == self._cached_post and other.dev == self._cached_dev
117 # Post family: V itself + any post-release of V.
118 return other.dev == self._cached_dev or other.post is not None
120 def _order_key(self) -> _BoundaryOrderKey:
121 """Sort key placing this boundary just above the versions it covers.
123 It extends ``V``'s comparison key ``(epoch, release, suffix)`` with
124 a trailing ``_BOUNDARY_INF`` local component, so the key sorts after
125 ``V`` and every ``V+local`` (whose keys carry a real, finite local
126 segment). ``suffix`` is the 6-int comparison suffix
127 ``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``.
129 For an AFTER_POSTS boundary the suffix is replaced with one whose
130 post number is ``_BOUNDARY_INF``, so the key also sorts after every
131 ``V.postN``. An AFTER_LOCALS boundary uses ``V``'s suffix unchanged.
132 """
133 version_key = self.version._key
134 suffix: _BoundaryOrderSuffix = version_key[2]
136 if self.kind == BoundaryKind.AFTER_POSTS:
137 suffix = (suffix[0], suffix[1], 1, _BOUNDARY_INF, 1, 0)
139 return version_key[0], version_key[1], suffix, _BOUNDARY_INF
141 def __eq__(self, other: object) -> bool:
142 # Key off the order key so equality matches the ``<`` / ``>`` order:
143 # ``AFTER_POSTS(1.0)`` and ``AFTER_POSTS(1.0.post1)`` are the same point.
144 if isinstance(other, BoundaryVersion):
145 return self._order_key() == other._order_key()
146 return NotImplemented
148 def __lt__(self, other: BoundaryVersion | Version) -> bool:
149 if isinstance(other, BoundaryVersion):
150 return self._order_key() < other._order_key()
151 # boundary < other_version iff V < other AND other not in family.
152 # The cheap V >= other path short-circuits before the family check.
153 if not (self.version < other):
154 return False
155 return not self._is_family(other)
157 def __gt__(self, other: BoundaryVersion | Version) -> bool:
158 # Defined directly to bypass functools.total_ordering's
159 # NotImplemented round-trip on reflected ``Version < boundary``.
160 if isinstance(other, BoundaryVersion):
161 return self._order_key() > other._order_key()
162 if self.version >= other:
163 return True
164 return self._is_family(other)
166 def __hash__(self) -> int:
167 # Keyed to ``__eq__`` (the order key), so equal boundaries hash equal.
168 return hash(self._order_key())
170 def __repr__(self) -> str:
171 return f"{self.__class__.__name__}({self.version!r}, {self.kind.name})"
174if TYPE_CHECKING:
175 _VersionOrBoundary = Union[Version, BoundaryVersion, None]
178@functools.total_ordering
179class LowerBound:
180 """Lower bound of a version range.
182 A version *v* of ``None`` means unbounded below (-inf).
183 At equal versions, ``[v`` sorts before ``(v`` because an inclusive
184 bound starts earlier.
185 """
187 __slots__ = ("_above", "inclusive", "version")
189 def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
190 self.version = version
191 self.inclusive = inclusive
192 # Pre-bind a predicate "is parsed at or above this lower
193 # bound?" for the hot filter / contains loops. One direct
194 # call per check, no operator-dispatch chain.
195 if version is None:
196 self._above: Callable[[Version], bool] | None = None
197 elif isinstance(version, BoundaryVersion):
198 # >V produces an AFTER_POSTS lower bound; the upper-side
199 # range of !=V produces an AFTER_LOCALS lower bound.
200 if version.kind == BoundaryKind.AFTER_POSTS:
201 self._above = _make_above_after_posts(version.version)
202 else:
203 self._above = _make_above_after_locals(version.version)
204 elif inclusive:
205 self._above = version.__le__
206 else:
207 self._above = version.__lt__
209 def __eq__(self, other: object) -> bool:
210 if not isinstance(other, LowerBound):
211 return NotImplemented
212 return self.version == other.version and self.inclusive == other.inclusive
214 def __lt__(self, other: LowerBound) -> bool:
215 if not isinstance(other, LowerBound):
216 return NotImplemented
217 # -inf < anything (except -inf itself).
218 if self.version is None:
219 return other.version is not None
220 if other.version is None:
221 return False
222 if self.version != other.version:
223 return self.version < other.version
224 # [v < (v: inclusive starts earlier.
225 return self.inclusive and not other.inclusive
227 def __hash__(self) -> int:
228 return hash((self.version, self.inclusive))
230 def __repr__(self) -> str:
231 bracket = "[" if self.inclusive else "("
232 return f"<{self.__class__.__name__} {bracket}{self.version!r}>"
235@functools.total_ordering
236class UpperBound:
237 """Upper bound of a version range.
239 A version *v* of ``None`` means unbounded above (+inf).
240 At equal versions, ``v)`` sorts before ``v]`` because an exclusive
241 bound ends earlier.
242 """
244 __slots__ = ("_below", "inclusive", "version")
246 def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
247 self.version = version
248 self.inclusive = inclusive
249 # Pre-bind a predicate "is parsed at or below this upper
250 # bound?". See LowerBound for the rationale.
251 if version is None:
252 self._below: Callable[[Version], bool] | None = None
253 elif isinstance(version, BoundaryVersion):
254 # Standard specifiers only ever produce AFTER_LOCALS upper
255 # bounds (from <=V / ==V / !=V with no local).
256 if version.kind == BoundaryKind.AFTER_LOCALS:
257 self._below = _make_below_after_locals(version.version)
258 else:
259 # An AFTER_POSTS upper is not produced by any specifier, but
260 # range algebra reaches it: complementing ``>V`` flips the
261 # ``AFTER_POSTS(V)`` lower into this upper bound.
262 self._below = version.__ge__
263 elif inclusive:
264 self._below = version.__ge__
265 else:
266 self._below = version.__gt__
268 def __eq__(self, other: object) -> bool:
269 if not isinstance(other, UpperBound):
270 return NotImplemented
271 return self.version == other.version and self.inclusive == other.inclusive
273 def __lt__(self, other: UpperBound) -> bool:
274 if not isinstance(other, UpperBound):
275 return NotImplemented
276 # Nothing < +inf (except +inf itself).
277 if self.version is None:
278 return False
279 if other.version is None:
280 return True
281 if self.version != other.version:
282 return self.version < other.version
283 # v) < v]: exclusive ends earlier.
284 return not self.inclusive and other.inclusive
286 def __hash__(self) -> int:
287 return hash((self.version, self.inclusive))
289 def __repr__(self) -> str:
290 bracket = "]" if self.inclusive else ")"
291 return f"<{self.__class__.__name__} {self.version!r}{bracket}>"
294if TYPE_CHECKING:
295 #: A single contiguous version range, as a (lower, upper) pair.
296 VersionRange = tuple[LowerBound, UpperBound]
299NEG_INF: Final[LowerBound] = LowerBound(None, False)
300POS_INF: Final[UpperBound] = UpperBound(None, False)
301FULL_RANGE: Final[tuple[VersionRange]] = ((NEG_INF, POS_INF),)
304def trim_release(release: tuple[int, ...]) -> tuple[int, ...]:
305 """Strip trailing zeros from a release tuple for normalized comparison."""
306 end = len(release)
307 while end > 1 and release[end - 1] == 0:
308 end -= 1
309 return release if end == len(release) else release[:end]
312def _next_prefix_dev0(version: Version) -> Version:
313 """Smallest version in the next prefix: 1.2 -> 1.3.dev0."""
314 release = (*version.release[:-1], version.release[-1] + 1)
315 return Version.from_parts(epoch=version.epoch, release=release, dev=0)
318def _base_dev0(version: Version) -> Version:
319 """The .dev0 of a version's base release: 1.2 -> 1.2.dev0."""
320 return Version.from_parts(epoch=version.epoch, release=version.release, dev=0)
323def coerce_version(version: Version | str) -> Version | None:
324 if not isinstance(version, Version):
325 try:
326 version = Version(version)
327 except InvalidVersion:
328 return None
329 return version
332def _make_above_after_posts(version: Version) -> Callable[[Version], bool]:
333 """Predicate ``parsed > AFTER_POSTS(V)`` for a lower bound.
335 Per PEP 440, ``>V`` excludes V's post-releases unless V is itself
336 a post-release. AFTER_POSTS sits above V and every V.postN (with
337 or without local), and just below the next release.
338 """
339 version_ge = version.__ge__
340 version_epoch = version.epoch
341 version_pre = version.pre
342 version_release_trimmed = trim_release(version.release)
343 trimmed_length = len(version_release_trimmed)
345 def above(parsed: Version) -> bool:
346 if version_ge(parsed):
347 return False
348 # parsed > V cmpkey-wise: above the boundary iff NOT in V's
349 # post family.
350 if parsed.epoch != version_epoch:
351 return True
352 parsed_release = parsed.release
353 if len(parsed_release) < trimmed_length:
354 return True
355 if parsed_release[:trimmed_length] != version_release_trimmed:
356 return True
357 for i in range(trimmed_length, len(parsed_release)):
358 if parsed_release[i] != 0:
359 return True
360 if parsed.pre != version_pre:
361 return True
363 # Same release and pre as V: parsed is in V's post family (V itself,
364 # V+local, or V.postN), which the boundary sits above. A V.devN
365 # (different dev, no post) sorts before V and was already caught by
366 # ``version_ge`` above, so the answer here is always "not above".
367 return False
369 return above
372def _make_above_after_locals(version: Version) -> Callable[[Version], bool]:
373 """Predicate ``parsed > AFTER_LOCALS(V)`` for a lower bound.
375 Used by the upper-side range of ``!=V`` (when V has no local
376 segment). AFTER_LOCALS sits above V and every ``V+local`` but
377 just below ``V.post0``.
378 """
379 version_ge = version.__ge__
380 version_epoch = version.epoch
381 version_pre = version.pre
382 version_post = version.post
383 version_dev = version.dev
384 version_release_trimmed = trim_release(version.release)
385 trimmed_length = len(version_release_trimmed)
387 def above(parsed: Version) -> bool:
388 if version_ge(parsed):
389 return False
390 # parsed > V cmpkey-wise: above the boundary iff NOT in V's
391 # local family (same public version, any local segment).
392 if parsed.epoch != version_epoch:
393 return True
394 parsed_release = parsed.release
395 if len(parsed_release) < trimmed_length:
396 return True
397 if parsed_release[:trimmed_length] != version_release_trimmed:
398 return True
399 for i in range(trimmed_length, len(parsed_release)):
400 if parsed_release[i] != 0:
401 return True
402 if parsed.pre != version_pre:
403 return True
404 if parsed.post != version_post:
405 return True
406 return parsed.dev != version_dev
408 return above
411def _make_below_after_locals(version: Version) -> Callable[[Version], bool]:
412 """Predicate ``parsed <= AFTER_LOCALS(V)`` for an upper bound.
414 Used by ``<=V``, ``==V``, ``!=V`` (no local). ``parsed`` is at or
415 below the boundary when it is at or below V cmpkey-wise, or when
416 it is in V's local family.
417 """
418 version_ge = version.__ge__
419 version_epoch = version.epoch
420 version_pre = version.pre
421 version_post = version.post
422 version_dev = version.dev
423 version_release_trimmed = trim_release(version.release)
424 trimmed_length = len(version_release_trimmed)
426 def below(parsed: Version) -> bool:
427 if version_ge(parsed):
428 return True
429 # parsed > V cmpkey-wise: below the boundary iff in V's local
430 # family.
431 if parsed.epoch != version_epoch:
432 return False
433 parsed_release = parsed.release
434 if len(parsed_release) < trimmed_length:
435 return False
436 if parsed_release[:trimmed_length] != version_release_trimmed:
437 return False
438 for i in range(trimmed_length, len(parsed_release)):
439 if parsed_release[i] != 0:
440 return False
441 if parsed.pre != version_pre:
442 return False
443 if parsed.post != version_post:
444 return False
445 return parsed.dev == version_dev
447 return below
450def least_version_above(boundary: BoundaryVersion) -> Version | None:
451 """Smallest real version strictly above *boundary*, or ``None`` if none exists."""
452 base = boundary.version
454 if boundary.kind == BoundaryKind.AFTER_LOCALS:
455 # AFTER_LOCALS(V) sits just below V.post0, so its least successor is
456 # V.post0.dev0 (V.dev(N+1) if V has a dev, V.post(N+1).dev0 if a post).
457 if base.dev is not None:
458 return base.__replace__(dev=base.dev + 1, local=None)
459 next_post = (base.post + 1) if base.post is not None else 0
460 return base.__replace__(post=next_post, dev=0, local=None)
462 # AFTER_POSTS(V): a pre-release V steps to the next pre-release's .dev0;
463 # a final-release AFTER_POSTS has no least successor.
464 if base.pre is not None:
465 kind, number = base.pre
466 return base.__replace__(pre=(kind, number + 1), post=None, dev=0, local=None)
468 return None
471def range_is_empty(lower: LowerBound, upper: UpperBound) -> bool:
472 """True when the range defined by *lower* and *upper* contains no versions.
474 A boundary lower sits just below the next real version, so an ordered pair
475 is still empty when the upper excludes that least successor:
476 ``(AFTER_POSTS(1.0a1), 1.0a2.dev0)`` holds no version.
477 """
478 if upper.version is None:
479 return False
481 if lower.version is None:
482 # Nothing sorts below MIN_VERSION, so an exclusive upper at or below it
483 # leaves an empty floor interval such as ``(-inf, 0.dev0)``.
484 return (
485 not upper.inclusive
486 and isinstance(upper.version, Version)
487 and upper.version <= MIN_VERSION
488 )
490 if isinstance(lower.version, BoundaryVersion):
491 successor = least_version_above(lower.version)
492 if successor is not None:
493 if upper.version == successor:
494 return not upper.inclusive
495 return upper.version < successor
497 if lower.version == upper.version:
498 return not (lower.inclusive and upper.inclusive)
500 return lower.version > upper.version
503def intersect_ranges(
504 left: Sequence[VersionRange],
505 right: Sequence[VersionRange],
506) -> list[VersionRange]:
507 """Intersect two sorted, non-overlapping range lists (two-pointer merge)."""
508 result: list[VersionRange] = []
509 left_index = right_index = 0
510 while left_index < len(left) and right_index < len(right):
511 left_lower, left_upper = left[left_index]
512 right_lower, right_upper = right[right_index]
514 lower = max(left_lower, right_lower)
515 upper = min(left_upper, right_upper)
517 if not range_is_empty(lower, upper):
518 result.append((lower, upper))
520 # Advance whichever side has the smaller upper bound.
521 if left_upper < right_upper:
522 left_index += 1
523 else:
524 right_index += 1
526 return result
529def filter_by_ranges(
530 ranges: Sequence[VersionRange],
531 iterable: Iterable[Any],
532 key: Callable[[Any], Version | str] | None,
533 prereleases: bool | None,
534) -> Iterator[Any]:
535 """Filter *iterable* against precomputed version *ranges*.
537 With ``prereleases=None``, the PEP 440 default applies: pre-releases
538 are excluded unless no final matches, in which case buffered
539 pre-releases come out at the end.
540 """
541 if prereleases is None:
542 # PEP 440 default: yield finals immediately; buffer
543 # pre-releases until at least one final has been emitted.
544 prerelease_buffer: list[Any] = []
545 found_final = False
547 if len(ranges) == 1:
548 lower, upper = ranges[0]
549 above = lower._above
550 below = upper._below
551 for item in iterable:
552 parsed = coerce_version(item if key is None else key(item))
553 if parsed is None:
554 continue
555 if above is not None and not above(parsed):
556 continue
557 if below is not None and not below(parsed):
558 continue
559 if parsed.is_prerelease:
560 if not found_final:
561 prerelease_buffer.append(item)
562 else:
563 found_final = True
564 yield item
565 if not found_final:
566 yield from prerelease_buffer
567 return
569 for item in iterable:
570 parsed = coerce_version(item if key is None else key(item))
571 if parsed is None:
572 continue
573 for lower, upper in ranges:
574 above = lower._above
575 if above is not None and not above(parsed):
576 break
577 below = upper._below
578 if below is None or below(parsed):
579 if parsed.is_prerelease:
580 if not found_final:
581 prerelease_buffer.append(item)
582 else:
583 found_final = True
584 yield item
585 break
586 if not found_final:
587 yield from prerelease_buffer
588 return
590 exclude_prereleases = prereleases is False
592 if len(ranges) == 1:
593 # Hot path: most specifiers and small SpecifierSets reduce to
594 # a single contiguous range.
595 lower, upper = ranges[0]
596 above = lower._above
597 below = upper._below
598 for item in iterable:
599 parsed = coerce_version(item if key is None else key(item))
600 if parsed is None:
601 continue
602 if exclude_prereleases and parsed.is_prerelease:
603 continue
604 if above is not None and not above(parsed):
605 continue
606 if below is None or below(parsed):
607 yield item
608 return
610 for item in iterable:
611 parsed = coerce_version(item if key is None else key(item))
612 if parsed is None:
613 continue
614 if exclude_prereleases and parsed.is_prerelease:
615 continue
616 for lower, upper in ranges:
617 above = lower._above
618 if above is not None and not above(parsed):
619 break
620 below = upper._below
621 if below is None or below(parsed):
622 yield item
623 break
626def _nearest_release_above_prerelease(version: Version) -> Version:
627 """Smallest non-pre-release at or above a pre-release *version*."""
628 if version.pre is not None:
629 # An a/b/rc pre-release drops to its final release, which outranks
630 # every post-release of that pre-release (1.0a1.post0 -> 1.0).
631 return version.__replace__(pre=None, post=None, dev=None, local=None)
633 # A dev-only release keeps its post-release (1.0.post0.dev0 -> 1.0.post0,
634 # whose final 1.0 sorts below it).
635 return version.__replace__(dev=None, local=None)
638def _lowest_release_at_or_above(value: Version | BoundaryVersion | None) -> Version:
639 """Smallest non-pre-release version at or above *value*.
641 ``None`` is the ``-inf`` floor, whose nearest non-pre-release is
642 :data:`MIN_RELEASE`.
643 """
644 if value is None:
645 return MIN_RELEASE
646 if isinstance(value, BoundaryVersion):
647 inner_version = value.version
648 if inner_version.is_prerelease:
649 return _nearest_release_above_prerelease(inner_version)
650 # AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0
651 # AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1
652 next_post = (inner_version.post + 1) if inner_version.post is not None else 0
653 return inner_version.__replace__(post=next_post, local=None)
655 if not value.is_prerelease:
656 return value
658 return _nearest_release_above_prerelease(value)
661def ranges_are_prerelease_only(ranges: Sequence[VersionRange]) -> bool:
662 """True when every range in *ranges* contains only pre-releases.
664 Used to detect unsatisfiable specifier sets when ``prereleases=False``:
665 if every range is pre-release-only, every contained version is excluded.
666 """
667 for lower, upper in ranges:
668 nearest = _lowest_release_at_or_above(lower.version)
669 if upper.version is None or nearest < upper.version:
670 return False
671 if nearest == upper.version and upper.inclusive:
672 return False
673 return True
676def wildcard_ranges(op: str, base: Version) -> list[VersionRange]:
677 """Ranges for ==V.* and !=V.*.
679 ==1.2.* -> [1.2.dev0, 1.3.dev0); !=1.2.* -> complement.
680 """
681 lower = _base_dev0(base)
682 upper = _next_prefix_dev0(base)
683 if op == "==":
684 return [(LowerBound(lower, True), UpperBound(upper, False))]
685 # !=
686 return [
687 (NEG_INF, UpperBound(lower, False)),
688 (LowerBound(upper, True), POS_INF),
689 ]
692def standard_ranges(op: str, version: Version, has_local: bool) -> list[VersionRange]:
693 """Ranges for the standard PEP 440 operators (no wildcard, no ===).
695 *has_local* indicates whether the spec string included a ``+local``
696 segment; relevant only for ``==`` / ``!=`` to decide whether the
697 upper bound includes V's local family.
698 """
699 if op == ">=":
700 return [(LowerBound(version, True), POS_INF)]
702 if op == "<=":
703 return [
704 (
705 NEG_INF,
706 UpperBound(BoundaryVersion(version, BoundaryKind.AFTER_LOCALS), True),
707 )
708 ]
710 if op == ">":
711 if version.dev is not None:
712 # >V.devN: dev versions have no post-releases, so the
713 # next real version is V.dev(N+1).
714 lower_bound = version.__replace__(dev=version.dev + 1, local=None)
715 return [(LowerBound(lower_bound, True), POS_INF)]
716 if version.post is not None:
717 # >V.postN: next real version is V.post(N+1).dev0.
718 lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None)
719 return [(LowerBound(lower_bound, True), POS_INF)]
720 # >V (final or pre-release V): exclude V itself, V+local, and
721 # every V.postN per PEP 440.
722 return [
723 (
724 LowerBound(BoundaryVersion(version, BoundaryKind.AFTER_POSTS), False),
725 POS_INF,
726 )
727 ]
729 if op == "<":
730 # <V excludes pre-releases of V when V is not a pre-release.
731 # V.dev0 is the earliest pre-release of V.
732 bound = (
733 version if version.is_prerelease else version.__replace__(dev=0, local=None)
734 )
735 if bound <= MIN_VERSION:
736 return []
737 return [(NEG_INF, UpperBound(bound, False))]
739 # ==, !=: local versions of V match when the spec has no local segment.
740 after_locals = BoundaryVersion(version, BoundaryKind.AFTER_LOCALS)
741 upper = version if has_local else after_locals
743 if op == "==":
744 return [(LowerBound(version, True), UpperBound(upper, True))]
746 if op == "!=":
747 return [
748 (NEG_INF, UpperBound(version, False)),
749 (LowerBound(upper, False), POS_INF),
750 ]
752 if op == "~=":
753 prefix = version.__replace__(release=version.release[:-1])
754 return [
755 (LowerBound(version, True), UpperBound(_next_prefix_dev0(prefix), False))
756 ]
758 raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover
761def bounds_for_spec(op: str, version_str: str, version: Version) -> list[VersionRange]:
762 """Ranges for one specifier's ``(op, version_str)``.
764 Dispatches between the wildcard and standard builders. ``version`` is the
765 parsed ``version_str`` (its base, without the trailing ``.*``, for
766 wildcards). ``===`` is not handled here; its match is a literal string
767 compared in :mod:`packaging.specifiers`.
768 """
769 if version_str.endswith(".*"):
770 return wildcard_ranges(op, version)
772 return standard_ranges(op, version, "+" in version_str)
775def intersect_specifier_bounds(
776 per_specifier_ranges: Iterable[Sequence[VersionRange]],
777) -> Sequence[VersionRange]:
778 """Intersect each specifier's ranges into a single sequence.
780 Short-circuits once the running intersection is empty, since no later
781 specifier can revive it. Callers must pass at least one specifier.
782 """
783 result: Sequence[VersionRange] | None = None
784 for sub in per_specifier_ranges:
785 if result is None:
786 result = sub
787 else:
788 result = intersect_ranges(result, sub)
789 if not result:
790 break
792 if result is None: # pragma: no cover - callers guard non-empty input
793 raise RuntimeError("intersect_specifier_bounds called with no specifiers")
795 return result
798def matches_bounds_only(ranges: Sequence[VersionRange], version: Version) -> bool:
799 """Whether ``version`` falls within any of ``ranges``.
801 The pure bounds membership test, for a single already-parsed version with
802 no pre-release policy applied. ``ranges`` are sorted and non-overlapping,
803 so a version below one range's lower bound is below every later range too.
804 """
805 for lower, upper in ranges:
806 above = lower._above
807 if above is not None and not above(version):
808 return False
810 below = upper._below
811 if below is None or below(version):
812 return True
814 return False
817def resolve_prereleases(
818 configured: bool | None, autodetected: bool | None
819) -> bool | None:
820 """Resolve a specifier's effective default pre-release policy.
822 An explicit ``configured`` value wins; otherwise an autodetected ``True``
823 propagates and anything else falls back to the PEP 440 default (``None``).
824 """
825 if configured is not None:
826 return configured
828 if autodetected:
829 return True
831 return None