Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/_ranges.py: 23%
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
21 # Total-order key for comparing two boundaries (boundary-vs-boundary only).
22 # The post slot may be ``_BOUNDARY_INF`` for an AFTER_POSTS boundary.
23 _BoundaryOrderSuffix = tuple[int, int, int, int | float, int, int]
24 _BoundaryOrderKey = tuple[int, tuple[int, ...], _BoundaryOrderSuffix, float]
26__all__ = [
27 "FULL_RANGE",
28 "bounds_for_spec",
29 "coerce_version",
30 "filter_by_ranges",
31 "intersect_ranges",
32 "intersect_specifier_bounds",
33 "least_version_above",
34 "matches_bounds_only",
35 "range_is_empty",
36 "ranges_are_prerelease_only",
37 "resolve_prereleases",
38 "standard_ranges",
39 "wildcard_ranges",
40]
42#: The smallest possible PEP 440 version. No valid version is less than this.
43MIN_VERSION: Final[Version] = Version("0.dev0")
45#: The smallest non-pre-release version, i.e. the nearest non-pre-release at or
46#: above the ``-inf`` floor.
47MIN_RELEASE: Final[Version] = Version("0")
49#: Sorts above any real post number and any local label, so a boundary can be
50#: ordered above the version family it covers when two boundaries are compared.
51_BOUNDARY_INF: Final[float] = float("inf")
54class BoundaryKind(enum.Enum):
55 """Where a boundary marker sits in the version ordering."""
57 AFTER_LOCALS = enum.auto() # after V+local, before V.post0
58 AFTER_POSTS = enum.auto() # after V.postN, before next release
61@functools.total_ordering
62class BoundaryVersion:
63 """A point on the version line between two real PEP 440 versions.
65 Relative to a base version V::
67 V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V)
69 AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no
70 local), and the lower bound of the upper-side range of ``!=V``.
71 AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release),
72 excluding V's post-releases per PEP 440.
73 """
75 __slots__ = (
76 "_cached_dev",
77 "_cached_epoch",
78 "_cached_post",
79 "_cached_pre",
80 "_cached_trimmed_release",
81 "kind",
82 "version",
83 )
85 def __init__(self, version: Version, kind: BoundaryKind) -> None:
86 self.version = version
87 self.kind = kind
88 self._cached_trimmed_release = trim_release(version.release)
89 self._cached_epoch = version.epoch
90 self._cached_pre = version.pre
91 self._cached_post = version.post
92 self._cached_dev = version.dev
94 def _is_family(self, other: Version) -> bool:
95 """Is ``other`` a version that this boundary sorts above?"""
96 if other.epoch != self._cached_epoch:
97 return False
98 # Inline release-trim comparison: other.release matches the
99 # trimmed release iff its leading slice is equal and any extra
100 # components are zero. Avoids trim_release's tuple allocation.
101 other_release = other.release
102 trimmed_release = self._cached_trimmed_release
103 trimmed_length = len(trimmed_release)
104 if len(other_release) < trimmed_length:
105 return False
106 if other_release[:trimmed_length] != trimmed_release:
107 return False
108 for i in range(trimmed_length, len(other_release)):
109 if other_release[i] != 0:
110 return False
111 if other.pre != self._cached_pre:
112 return False
113 if self.kind == BoundaryKind.AFTER_LOCALS:
114 # Local family: same public version, any local label.
115 return other.post == self._cached_post and other.dev == self._cached_dev
116 # Post family: V itself + any post-release of V.
117 return other.dev == self._cached_dev or other.post is not None
119 def _order_key(self) -> _BoundaryOrderKey:
120 """Sort key placing this boundary just above the versions it covers.
122 It extends ``V``'s comparison key ``(epoch, release, suffix)`` with
123 a trailing ``_BOUNDARY_INF`` local component, so the key sorts after
124 ``V`` and every ``V+local`` (whose keys carry a real, finite local
125 segment). ``suffix`` is the 6-int comparison suffix
126 ``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``.
128 For an AFTER_POSTS boundary the suffix is replaced with one whose
129 post number is ``_BOUNDARY_INF``, so the key also sorts after every
130 ``V.postN``. An AFTER_LOCALS boundary uses ``V``'s suffix unchanged.
131 """
132 version_key = self.version._key
133 suffix: _BoundaryOrderSuffix = version_key[2]
135 if self.kind == BoundaryKind.AFTER_POSTS:
136 suffix = (suffix[0], suffix[1], 1, _BOUNDARY_INF, 1, 0)
138 return version_key[0], version_key[1], suffix, _BOUNDARY_INF
140 def __eq__(self, other: object) -> bool:
141 # Key off the order key so equality matches the ``<`` / ``>`` order:
142 # ``AFTER_POSTS(1.0)`` and ``AFTER_POSTS(1.0.post1)`` are the same point.
143 if isinstance(other, BoundaryVersion):
144 return self._order_key() == other._order_key()
145 return NotImplemented
147 def __lt__(self, other: BoundaryVersion | Version) -> bool:
148 if isinstance(other, BoundaryVersion):
149 return self._order_key() < other._order_key()
150 # boundary < other_version iff V < other AND other not in family.
151 # The cheap V >= other path short-circuits before the family check.
152 if not (self.version < other):
153 return False
154 return not self._is_family(other)
156 def __gt__(self, other: BoundaryVersion | Version) -> bool:
157 # Defined directly to bypass functools.total_ordering's
158 # NotImplemented round-trip on reflected ``Version < boundary``.
159 if isinstance(other, BoundaryVersion):
160 return self._order_key() > other._order_key()
161 if self.version >= other:
162 return True
163 return self._is_family(other)
165 def __hash__(self) -> int:
166 # Keyed to ``__eq__`` (the order key), so equal boundaries hash equal.
167 return hash(self._order_key())
169 def __repr__(self) -> str:
170 return f"{self.__class__.__name__}({self.version!r}, {self.kind.name})"
173if TYPE_CHECKING:
174 _VersionOrBoundary = Version | BoundaryVersion | None
177@functools.total_ordering
178class LowerBound:
179 """Lower bound of a version range.
181 A version *v* of ``None`` means unbounded below (-inf), with
182 ``inclusive`` forced to ``False``.
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 # Two spellings of -inf would sort above each other under total_ordering.
191 if version is None:
192 inclusive = False
194 self.version = version
195 self.inclusive = inclusive
196 # Pre-bind a predicate "is parsed at or above this lower
197 # bound?" for the hot filter / contains loops. One direct
198 # call per check, no operator-dispatch chain.
199 if version is None:
200 self._above: Callable[[Version], bool] | None = None
201 elif isinstance(version, BoundaryVersion):
202 # >V produces an AFTER_POSTS lower bound; the upper-side
203 # range of !=V produces an AFTER_LOCALS lower bound.
204 if version.kind == BoundaryKind.AFTER_POSTS:
205 self._above = _make_above_after_posts(version.version)
206 else:
207 self._above = _make_above_after_locals(version.version)
208 elif inclusive:
209 self._above = version.__le__
210 else:
211 self._above = version.__lt__
213 def __eq__(self, other: object) -> bool:
214 if not isinstance(other, LowerBound):
215 return NotImplemented
216 return self.version == other.version and self.inclusive == other.inclusive
218 def __lt__(self, other: LowerBound) -> bool:
219 if not isinstance(other, LowerBound):
220 return NotImplemented
221 # -inf < anything (except -inf itself).
222 if self.version is None:
223 return other.version is not None
224 if other.version is None:
225 return False
226 if self.version != other.version:
227 return self.version < other.version
228 # [v < (v: inclusive starts earlier.
229 return self.inclusive and not other.inclusive
231 def __hash__(self) -> int:
232 return hash((self.version, self.inclusive))
234 def __repr__(self) -> str:
235 bracket = "[" if self.inclusive else "("
236 return f"<{self.__class__.__name__} {bracket}{self.version!r}>"
239@functools.total_ordering
240class UpperBound:
241 """Upper bound of a version range.
243 A version *v* of ``None`` means unbounded above (+inf), with
244 ``inclusive`` forced to ``False``.
245 At equal versions, ``v)`` sorts before ``v]`` because an exclusive
246 bound ends earlier.
247 """
249 __slots__ = ("_below", "inclusive", "version")
251 def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
252 # Two spellings of +inf would sort above each other under total_ordering.
253 if version is None:
254 inclusive = False
256 self.version = version
257 self.inclusive = inclusive
258 # Pre-bind a predicate "is parsed at or below this upper
259 # bound?". See LowerBound for the rationale.
260 if version is None:
261 self._below: Callable[[Version], bool] | None = None
262 elif isinstance(version, BoundaryVersion):
263 # Standard specifiers only ever produce AFTER_LOCALS upper
264 # bounds (from <=V / ==V / !=V with no local).
265 if version.kind == BoundaryKind.AFTER_LOCALS:
266 self._below = _make_below_after_locals(version.version)
267 else:
268 # An AFTER_POSTS upper is not produced by any specifier, but
269 # range algebra reaches it: complementing ``>V`` flips the
270 # ``AFTER_POSTS(V)`` lower into this upper bound.
271 self._below = version.__ge__
272 elif inclusive:
273 self._below = version.__ge__
274 else:
275 self._below = version.__gt__
277 def __eq__(self, other: object) -> bool:
278 if not isinstance(other, UpperBound):
279 return NotImplemented
280 return self.version == other.version and self.inclusive == other.inclusive
282 def __lt__(self, other: UpperBound) -> bool:
283 if not isinstance(other, UpperBound):
284 return NotImplemented
285 # Nothing < +inf (except +inf itself).
286 if self.version is None:
287 return False
288 if other.version is None:
289 return True
290 if self.version != other.version:
291 return self.version < other.version
292 # v) < v]: exclusive ends earlier.
293 return not self.inclusive and other.inclusive
295 def __hash__(self) -> int:
296 return hash((self.version, self.inclusive))
298 def __repr__(self) -> str:
299 bracket = "]" if self.inclusive else ")"
300 return f"<{self.__class__.__name__} {self.version!r}{bracket}>"
303if TYPE_CHECKING:
304 #: A single contiguous interval as a (lower, upper) bound pair.
305 Interval = tuple[LowerBound, UpperBound]
308NEG_INF: Final[LowerBound] = LowerBound(None, False)
309POS_INF: Final[UpperBound] = UpperBound(None, False)
310FULL_RANGE: Final[tuple[Interval]] = ((NEG_INF, POS_INF),)
313def trim_release(release: tuple[int, ...]) -> tuple[int, ...]:
314 """Strip trailing zeros from a release tuple for normalized comparison."""
315 end = len(release)
316 while end > 1 and release[end - 1] == 0:
317 end -= 1
318 return release if end == len(release) else release[:end]
321def _next_prefix_dev0(version: Version) -> Version:
322 """Smallest version in the next prefix: 1.2 -> 1.3.dev0."""
323 release = (*version.release[:-1], version.release[-1] + 1)
324 return Version.from_parts(epoch=version.epoch, release=release, dev=0)
327def _base_dev0(version: Version) -> Version:
328 """The .dev0 of a version's base release: 1.2 -> 1.2.dev0."""
329 return Version.from_parts(epoch=version.epoch, release=version.release, dev=0)
332def coerce_version(version: Version | str) -> Version | None:
333 if not isinstance(version, Version):
334 try:
335 version = Version(version)
336 except InvalidVersion:
337 return None
338 return version
341def _make_above_after_posts(version: Version) -> Callable[[Version], bool]:
342 """Predicate ``parsed > AFTER_POSTS(V)`` for a lower bound.
344 Per PEP 440, ``>V`` excludes V's post-releases unless V is itself
345 a post-release. AFTER_POSTS sits above V and every V.postN (with
346 or without local), and just below the next release.
347 """
348 version_ge = version.__ge__
349 version_epoch = version.epoch
350 version_pre = version.pre
351 version_release_trimmed = trim_release(version.release)
352 trimmed_length = len(version_release_trimmed)
354 def above(parsed: Version) -> bool:
355 if version_ge(parsed):
356 return False
357 # parsed > V cmpkey-wise: above the boundary iff NOT in V's
358 # post family.
359 if parsed.epoch != version_epoch:
360 return True
361 parsed_release = parsed.release
362 if len(parsed_release) < trimmed_length:
363 return True
364 if parsed_release[:trimmed_length] != version_release_trimmed:
365 return True
366 for i in range(trimmed_length, len(parsed_release)):
367 if parsed_release[i] != 0:
368 return True
369 if parsed.pre != version_pre:
370 return True
372 # Same release and pre as V: parsed is in V's post family (V itself,
373 # V+local, or V.postN), which the boundary sits above. A V.devN
374 # (different dev, no post) sorts before V and was already caught by
375 # ``version_ge`` above, so the answer here is always "not above".
376 return False
378 return above
381def _make_above_after_locals(version: Version) -> Callable[[Version], bool]:
382 """Predicate ``parsed > AFTER_LOCALS(V)`` for a lower bound.
384 Used by the upper-side range of ``!=V`` (when V has no local
385 segment). AFTER_LOCALS sits above V and every ``V+local`` but
386 just below ``V.post0``.
387 """
388 version_ge = version.__ge__
389 version_epoch = version.epoch
390 version_pre = version.pre
391 version_post = version.post
392 version_dev = version.dev
393 version_release_trimmed = trim_release(version.release)
394 trimmed_length = len(version_release_trimmed)
396 def above(parsed: Version) -> bool:
397 if version_ge(parsed):
398 return False
399 # parsed > V cmpkey-wise: above the boundary iff NOT in V's
400 # local family (same public version, any local segment).
401 if parsed.epoch != version_epoch:
402 return True
403 parsed_release = parsed.release
404 if len(parsed_release) < trimmed_length:
405 return True
406 if parsed_release[:trimmed_length] != version_release_trimmed:
407 return True
408 for i in range(trimmed_length, len(parsed_release)):
409 if parsed_release[i] != 0:
410 return True
411 if parsed.pre != version_pre:
412 return True
413 if parsed.post != version_post:
414 return True
415 return parsed.dev != version_dev
417 return above
420def _make_below_after_locals(version: Version) -> Callable[[Version], bool]:
421 """Predicate ``parsed <= AFTER_LOCALS(V)`` for an upper bound.
423 Used by ``<=V``, ``==V``, ``!=V`` (no local). ``parsed`` is at or
424 below the boundary when it is at or below V cmpkey-wise, or when
425 it is in V's local family.
426 """
427 version_ge = version.__ge__
428 version_epoch = version.epoch
429 version_pre = version.pre
430 version_post = version.post
431 version_dev = version.dev
432 version_release_trimmed = trim_release(version.release)
433 trimmed_length = len(version_release_trimmed)
435 def below(parsed: Version) -> bool:
436 if version_ge(parsed):
437 return True
438 # parsed > V cmpkey-wise: below the boundary iff in V's local
439 # family.
440 if parsed.epoch != version_epoch:
441 return False
442 parsed_release = parsed.release
443 if len(parsed_release) < trimmed_length:
444 return False
445 if parsed_release[:trimmed_length] != version_release_trimmed:
446 return False
447 for i in range(trimmed_length, len(parsed_release)):
448 if parsed_release[i] != 0:
449 return False
450 if parsed.pre != version_pre:
451 return False
452 if parsed.post != version_post:
453 return False
454 return parsed.dev == version_dev
456 return below
459def least_version_above(boundary: BoundaryVersion) -> Version | None:
460 """Smallest real version strictly above *boundary*, or ``None`` if none exists."""
461 base = boundary.version
463 if boundary.kind == BoundaryKind.AFTER_LOCALS:
464 # AFTER_LOCALS(V) sits just below V.post0, so its least successor is
465 # V.post0.dev0 (V.dev(N+1) if V has a dev, V.post(N+1).dev0 if a post).
466 if base.dev is not None:
467 return base.__replace__(dev=base.dev + 1, local=None)
468 next_post = (base.post + 1) if base.post is not None else 0
469 return base.__replace__(post=next_post, dev=0, local=None)
471 # AFTER_POSTS(V): a pre-release V steps to the next pre-release's .dev0;
472 # a final-release AFTER_POSTS has no least successor.
473 if base.pre is not None:
474 kind, number = base.pre
475 return base.__replace__(pre=(kind, number + 1), post=None, dev=0, local=None)
477 return None
480def range_is_empty(lower: LowerBound, upper: UpperBound) -> bool:
481 """True when the range defined by *lower* and *upper* contains no versions.
483 A boundary lower sits just below the next real version, so an ordered pair
484 is still empty when the upper excludes that least successor:
485 ``(AFTER_POSTS(1.0a1), 1.0a2.dev0)`` holds no version.
486 """
487 if upper.version is None:
488 return False
490 if lower.version is None:
491 # Nothing sorts below MIN_VERSION, so an exclusive upper at or below it
492 # leaves an empty floor interval such as ``(-inf, 0.dev0)``.
493 return (
494 not upper.inclusive
495 and isinstance(upper.version, Version)
496 and upper.version <= MIN_VERSION
497 )
499 if isinstance(lower.version, BoundaryVersion):
500 successor = least_version_above(lower.version)
501 if successor is not None:
502 if upper.version == successor:
503 return not upper.inclusive
504 return upper.version < successor
506 if lower.version == upper.version:
507 return not (lower.inclusive and upper.inclusive)
509 return lower.version > upper.version
512def intersect_ranges(
513 left: Sequence[Interval],
514 right: Sequence[Interval],
515) -> list[Interval]:
516 """Intersect two sorted, non-overlapping range lists (two-pointer merge)."""
517 result: list[Interval] = []
518 left_index = right_index = 0
519 while left_index < len(left) and right_index < len(right):
520 left_lower, left_upper = left[left_index]
521 right_lower, right_upper = right[right_index]
523 lower = max(left_lower, right_lower)
524 upper = min(left_upper, right_upper)
526 if not range_is_empty(lower, upper):
527 result.append((lower, upper))
529 # Advance whichever side has the smaller upper bound.
530 if left_upper < right_upper:
531 left_index += 1
532 else:
533 right_index += 1
535 return result
538def filter_by_ranges(
539 ranges: Sequence[Interval],
540 iterable: Iterable[Any],
541 key: Callable[[Any], Version | str] | None,
542 prereleases: bool | None,
543 region: Sequence[Interval] = (),
544) -> Iterator[Any]:
545 """Filter *iterable* against precomputed version *ranges*.
547 With ``prereleases=None``, the PEP 440 default applies: pre-releases are
548 excluded unless no final matches, in which case buffered pre-releases come
549 out at the end. A pre-release inside the opt-in ``region`` is the exception:
550 it is force-admitted in place, as ``prereleases=True`` would yield it. A
551 force-admitted pre-release is not a final, so it never suppresses the buffer.
552 """
553 if prereleases is None:
554 prerelease_buffer: list[Any] = []
555 found_final = False
557 if len(ranges) == 1:
558 # Hot path: most specifiers and small SpecifierSets reduce to
559 # a single contiguous range.
560 lower, upper = ranges[0]
561 above = lower._above
562 below = upper._below
563 for item in iterable:
564 parsed = coerce_version(item if key is None else key(item))
565 if parsed is None:
566 continue
567 if above is not None and not above(parsed):
568 continue
569 if below is not None and not below(parsed):
570 continue
571 if not parsed.is_prerelease:
572 found_final = True
573 yield item
574 elif region and matches_bounds_only(region, parsed):
575 yield item
576 elif not found_final:
577 prerelease_buffer.append(item)
578 if not found_final:
579 yield from prerelease_buffer
580 return
582 for item in iterable:
583 parsed = coerce_version(item if key is None else key(item))
584 if parsed is None:
585 continue
586 for lower, upper in ranges:
587 above = lower._above
588 if above is not None and not above(parsed):
589 break
590 below = upper._below
591 if below is None or below(parsed):
592 if not parsed.is_prerelease:
593 found_final = True
594 yield item
595 elif region and matches_bounds_only(region, parsed):
596 yield item
597 elif not found_final:
598 prerelease_buffer.append(item)
599 break
600 if not found_final:
601 yield from prerelease_buffer
602 return
604 exclude_prereleases = prereleases is False
606 if len(ranges) == 1:
607 # Hot path: most specifiers and small SpecifierSets reduce to
608 # a single contiguous range.
609 lower, upper = ranges[0]
610 above = lower._above
611 below = upper._below
612 for item in iterable:
613 parsed = coerce_version(item if key is None else key(item))
614 if parsed is None:
615 continue
616 if exclude_prereleases and parsed.is_prerelease:
617 continue
618 if above is not None and not above(parsed):
619 continue
620 if below is None or below(parsed):
621 yield item
622 return
624 for item in iterable:
625 parsed = coerce_version(item if key is None else key(item))
626 if parsed is None:
627 continue
628 if exclude_prereleases and parsed.is_prerelease:
629 continue
630 for lower, upper in ranges:
631 above = lower._above
632 if above is not None and not above(parsed):
633 break
634 below = upper._below
635 if below is None or below(parsed):
636 yield item
637 break
640def _nearest_release_above_prerelease(version: Version) -> Version:
641 """Smallest non-pre-release at or above a pre-release *version*."""
642 if version.pre is not None:
643 # An a/b/rc pre-release drops to its final release, which outranks
644 # every post-release of that pre-release (1.0a1.post0 -> 1.0).
645 return version.__replace__(pre=None, post=None, dev=None, local=None)
647 # A dev-only release keeps its post-release (1.0.post0.dev0 -> 1.0.post0,
648 # whose final 1.0 sorts below it).
649 return version.__replace__(dev=None, local=None)
652def _lowest_release_at_or_above(value: Version | BoundaryVersion | None) -> Version:
653 """Smallest non-pre-release version at or above *value*.
655 ``None`` is the ``-inf`` floor, whose nearest non-pre-release is
656 :data:`MIN_RELEASE`.
657 """
658 if value is None:
659 return MIN_RELEASE
660 if isinstance(value, BoundaryVersion):
661 inner_version = value.version
662 if inner_version.is_prerelease:
663 return _nearest_release_above_prerelease(inner_version)
664 # AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0
665 # AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1
666 next_post = (inner_version.post + 1) if inner_version.post is not None else 0
667 return inner_version.__replace__(post=next_post, local=None)
669 if not value.is_prerelease:
670 return value
672 return _nearest_release_above_prerelease(value)
675def ranges_are_prerelease_only(ranges: Sequence[Interval]) -> bool:
676 """True when every range in *ranges* contains only pre-releases.
678 Used to detect unsatisfiable specifier sets when ``prereleases=False``:
679 if every range is pre-release-only, every contained version is excluded.
680 """
681 for lower, upper in ranges:
682 nearest = _lowest_release_at_or_above(lower.version)
683 if upper.version is None or nearest < upper.version:
684 return False
685 if nearest == upper.version and upper.inclusive:
686 return False
687 return True
690def wildcard_ranges(op: str, base: Version) -> list[Interval]:
691 """Ranges for ==V.* and !=V.*.
693 ==1.2.* -> [1.2.dev0, 1.3.dev0); !=1.2.* -> complement.
694 """
695 lower = _base_dev0(base)
696 upper = _next_prefix_dev0(base)
697 if op == "==":
698 return [(LowerBound(lower, True), UpperBound(upper, False))]
699 # !=
700 return [
701 (NEG_INF, UpperBound(lower, False)),
702 (LowerBound(upper, True), POS_INF),
703 ]
706def standard_ranges(op: str, version: Version, has_local: bool) -> list[Interval]:
707 """Ranges for the standard PEP 440 operators (no wildcard, no ===).
709 *has_local* indicates whether the spec string included a ``+local``
710 segment; relevant only for ``==`` / ``!=`` to decide whether the
711 upper bound includes V's local family.
712 """
713 if op == ">=":
714 return [(LowerBound(version, True), POS_INF)]
716 if op == "<=":
717 return [
718 (
719 NEG_INF,
720 UpperBound(BoundaryVersion(version, BoundaryKind.AFTER_LOCALS), True),
721 )
722 ]
724 if op == ">":
725 if version.dev is not None:
726 # >V.devN: dev versions have no post-releases, so the
727 # next real version is V.dev(N+1).
728 lower_bound = version.__replace__(dev=version.dev + 1, local=None)
729 return [(LowerBound(lower_bound, True), POS_INF)]
730 if version.post is not None:
731 # >V.postN: next real version is V.post(N+1).dev0.
732 lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None)
733 return [(LowerBound(lower_bound, True), POS_INF)]
734 # >V (final or pre-release V): exclude V itself, V+local, and
735 # every V.postN per PEP 440.
736 return [
737 (
738 LowerBound(BoundaryVersion(version, BoundaryKind.AFTER_POSTS), False),
739 POS_INF,
740 )
741 ]
743 if op == "<":
744 # <V excludes pre-releases of V when V is not a pre-release.
745 # V.dev0 is the earliest pre-release of V.
746 bound = (
747 version if version.is_prerelease else version.__replace__(dev=0, local=None)
748 )
749 if bound <= MIN_VERSION:
750 return []
751 return [(NEG_INF, UpperBound(bound, False))]
753 # ==, !=: local versions of V match when the spec has no local segment.
754 after_locals = BoundaryVersion(version, BoundaryKind.AFTER_LOCALS)
755 upper = version if has_local else after_locals
757 if op == "==":
758 return [(LowerBound(version, True), UpperBound(upper, True))]
760 if op == "!=":
761 return [
762 (NEG_INF, UpperBound(version, False)),
763 (LowerBound(upper, False), POS_INF),
764 ]
766 if op == "~=":
767 prefix = version.__replace__(release=version.release[:-1])
768 return [
769 (LowerBound(version, True), UpperBound(_next_prefix_dev0(prefix), False))
770 ]
772 raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover
775def bounds_for_spec(op: str, version_str: str, version: Version) -> list[Interval]:
776 """Ranges for one specifier's ``(op, version_str)``.
778 Dispatches between the wildcard and standard builders. ``version`` is the
779 parsed ``version_str`` (its base, without the trailing ``.*``, for
780 wildcards). ``===`` is not handled here; its match is a literal string
781 compared in :mod:`packaging.specifiers`.
782 """
783 if version_str.endswith(".*"):
784 return wildcard_ranges(op, version)
786 return standard_ranges(op, version, "+" in version_str)
789def intersect_specifier_bounds(
790 per_specifier_ranges: Iterable[Sequence[Interval]],
791) -> Sequence[Interval]:
792 """Intersect each specifier's ranges into a single sequence.
794 Short-circuits once the running intersection is empty, since no later
795 specifier can revive it. Callers must pass at least one specifier.
796 """
797 result: Sequence[Interval] | None = None
798 for sub in per_specifier_ranges:
799 if result is None:
800 result = sub
801 else:
802 result = intersect_ranges(result, sub)
803 if not result:
804 break
806 if result is None: # pragma: no cover - callers guard non-empty input
807 raise RuntimeError("intersect_specifier_bounds called with no specifiers")
809 return result
812def matches_bounds_only(ranges: Sequence[Interval], version: Version) -> bool:
813 """Whether ``version`` falls within any of ``ranges``.
815 The pure bounds membership test, for a single already-parsed version with
816 no pre-release policy applied. ``ranges`` are sorted and non-overlapping,
817 so a version below one range's lower bound is below every later range too.
818 """
819 for lower, upper in ranges:
820 above = lower._above
821 if above is not None and not above(version):
822 return False
824 below = upper._below
825 if below is None or below(version):
826 return True
828 return False
831def resolve_prereleases(
832 configured: bool | None, autodetected: bool | None
833) -> bool | None:
834 """Resolve a specifier's effective default pre-release policy.
836 An explicit ``configured`` value wins; otherwise an autodetected ``True``
837 propagates and anything else falls back to the PEP 440 default (``None``).
838 """
839 if configured is not None:
840 return configured
842 if autodetected:
843 return True
845 return None