feat: ж/д слой + cant_swim в скоринг + профили вне модели
Определение специфических рекомендаций (матрица профилей §8):
1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС):
- /api/v1/water/{case_id} отдаёт railway=rail как LineString
(без service/industrial/military веток), кэш общий v2;
- railway_warning «перекрыть/проверить немедленно» по профилям;
- SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂.
2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения
радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»):
- раньше чекбокс влиял только на текст, в скоринге был пробел;
- derive в analyze._derive_profiles — работает и для closed_cases.
3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка
«вне поведенческой модели» с пояснением (vector_tasks B12:
профили без аналога не выдавать за учтённые); блок на фронте
в карточке здоровья.
Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м²
vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile).
This commit is contained in:
@@ -0,0 +1,836 @@
|
||||
# This file is dual licensed under the terms of the Apache License, Version
|
||||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
"""Private version-range helpers used by :mod:`packaging.specifiers`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import functools
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
)
|
||||
|
||||
from .version import InvalidVersion, Version
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from typing import Union
|
||||
|
||||
# Total-order key for comparing two boundaries (boundary-vs-boundary only).
|
||||
# The post slot may be ``_BOUNDARY_INF`` for an AFTER_POSTS boundary.
|
||||
_BoundaryOrderSuffix = tuple[int, int, int, Union[int, float], int, int]
|
||||
_BoundaryOrderKey = tuple[int, tuple[int, ...], _BoundaryOrderSuffix, float]
|
||||
|
||||
__all__ = [
|
||||
"FULL_RANGE",
|
||||
"bounds_for_spec",
|
||||
"coerce_version",
|
||||
"filter_by_ranges",
|
||||
"intersect_ranges",
|
||||
"intersect_specifier_bounds",
|
||||
"least_version_above",
|
||||
"matches_bounds_only",
|
||||
"range_is_empty",
|
||||
"ranges_are_prerelease_only",
|
||||
"resolve_prereleases",
|
||||
"standard_ranges",
|
||||
"wildcard_ranges",
|
||||
]
|
||||
|
||||
#: The smallest possible PEP 440 version. No valid version is less than this.
|
||||
MIN_VERSION: Final[Version] = Version("0.dev0")
|
||||
|
||||
#: The smallest non-pre-release version, i.e. the nearest non-pre-release at or
|
||||
#: above the ``-inf`` floor.
|
||||
MIN_RELEASE: Final[Version] = Version("0")
|
||||
|
||||
#: Sorts above any real post number and any local label, so a boundary can be
|
||||
#: ordered above the version family it covers when two boundaries are compared.
|
||||
_BOUNDARY_INF: Final[float] = float("inf")
|
||||
|
||||
|
||||
class BoundaryKind(enum.Enum):
|
||||
"""Where a boundary marker sits in the version ordering."""
|
||||
|
||||
AFTER_LOCALS = enum.auto() # after V+local, before V.post0
|
||||
AFTER_POSTS = enum.auto() # after V.postN, before next release
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class BoundaryVersion:
|
||||
"""A point on the version line between two real PEP 440 versions.
|
||||
|
||||
Relative to a base version V::
|
||||
|
||||
V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V)
|
||||
|
||||
AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no
|
||||
local), and the lower bound of the upper-side range of ``!=V``.
|
||||
AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release),
|
||||
excluding V's post-releases per PEP 440.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_cached_dev",
|
||||
"_cached_epoch",
|
||||
"_cached_post",
|
||||
"_cached_pre",
|
||||
"_cached_trimmed_release",
|
||||
"kind",
|
||||
"version",
|
||||
)
|
||||
|
||||
def __init__(self, version: Version, kind: BoundaryKind) -> None:
|
||||
self.version = version
|
||||
self.kind = kind
|
||||
self._cached_trimmed_release = trim_release(version.release)
|
||||
self._cached_epoch = version.epoch
|
||||
self._cached_pre = version.pre
|
||||
self._cached_post = version.post
|
||||
self._cached_dev = version.dev
|
||||
|
||||
def _is_family(self, other: Version) -> bool:
|
||||
"""Is ``other`` a version that this boundary sorts above?"""
|
||||
if other.epoch != self._cached_epoch:
|
||||
return False
|
||||
# Inline release-trim comparison: other.release matches the
|
||||
# trimmed release iff its leading slice is equal and any extra
|
||||
# components are zero. Avoids trim_release's tuple allocation.
|
||||
other_release = other.release
|
||||
trimmed_release = self._cached_trimmed_release
|
||||
trimmed_length = len(trimmed_release)
|
||||
if len(other_release) < trimmed_length:
|
||||
return False
|
||||
if other_release[:trimmed_length] != trimmed_release:
|
||||
return False
|
||||
for i in range(trimmed_length, len(other_release)):
|
||||
if other_release[i] != 0:
|
||||
return False
|
||||
if other.pre != self._cached_pre:
|
||||
return False
|
||||
if self.kind == BoundaryKind.AFTER_LOCALS:
|
||||
# Local family: same public version, any local label.
|
||||
return other.post == self._cached_post and other.dev == self._cached_dev
|
||||
# Post family: V itself + any post-release of V.
|
||||
return other.dev == self._cached_dev or other.post is not None
|
||||
|
||||
def _order_key(self) -> _BoundaryOrderKey:
|
||||
"""Sort key placing this boundary just above the versions it covers.
|
||||
|
||||
It extends ``V``'s comparison key ``(epoch, release, suffix)`` with
|
||||
a trailing ``_BOUNDARY_INF`` local component, so the key sorts after
|
||||
``V`` and every ``V+local`` (whose keys carry a real, finite local
|
||||
segment). ``suffix`` is the 6-int comparison suffix
|
||||
``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``.
|
||||
|
||||
For an AFTER_POSTS boundary the suffix is replaced with one whose
|
||||
post number is ``_BOUNDARY_INF``, so the key also sorts after every
|
||||
``V.postN``. An AFTER_LOCALS boundary uses ``V``'s suffix unchanged.
|
||||
"""
|
||||
version_key = self.version._key
|
||||
suffix: _BoundaryOrderSuffix = version_key[2]
|
||||
|
||||
if self.kind == BoundaryKind.AFTER_POSTS:
|
||||
suffix = (suffix[0], suffix[1], 1, _BOUNDARY_INF, 1, 0)
|
||||
|
||||
return version_key[0], version_key[1], suffix, _BOUNDARY_INF
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
# Key off the order key so equality matches the ``<`` / ``>`` order:
|
||||
# ``AFTER_POSTS(1.0)`` and ``AFTER_POSTS(1.0.post1)`` are the same point.
|
||||
if isinstance(other, BoundaryVersion):
|
||||
return self._order_key() == other._order_key()
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: BoundaryVersion | Version) -> bool:
|
||||
if isinstance(other, BoundaryVersion):
|
||||
return self._order_key() < other._order_key()
|
||||
# boundary < other_version iff V < other AND other not in family.
|
||||
# The cheap V >= other path short-circuits before the family check.
|
||||
if not (self.version < other):
|
||||
return False
|
||||
return not self._is_family(other)
|
||||
|
||||
def __gt__(self, other: BoundaryVersion | Version) -> bool:
|
||||
# Defined directly to bypass functools.total_ordering's
|
||||
# NotImplemented round-trip on reflected ``Version < boundary``.
|
||||
if isinstance(other, BoundaryVersion):
|
||||
return self._order_key() > other._order_key()
|
||||
if self.version >= other:
|
||||
return True
|
||||
return self._is_family(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Keyed to ``__eq__`` (the order key), so equal boundaries hash equal.
|
||||
return hash(self._order_key())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.version!r}, {self.kind.name})"
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_VersionOrBoundary = Union[Version, BoundaryVersion, None]
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class LowerBound:
|
||||
"""Lower bound of a version range.
|
||||
|
||||
A version *v* of ``None`` means unbounded below (-inf).
|
||||
At equal versions, ``[v`` sorts before ``(v`` because an inclusive
|
||||
bound starts earlier.
|
||||
"""
|
||||
|
||||
__slots__ = ("_above", "inclusive", "version")
|
||||
|
||||
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
|
||||
self.version = version
|
||||
self.inclusive = inclusive
|
||||
# Pre-bind a predicate "is parsed at or above this lower
|
||||
# bound?" for the hot filter / contains loops. One direct
|
||||
# call per check, no operator-dispatch chain.
|
||||
if version is None:
|
||||
self._above: Callable[[Version], bool] | None = None
|
||||
elif isinstance(version, BoundaryVersion):
|
||||
# >V produces an AFTER_POSTS lower bound; the upper-side
|
||||
# range of !=V produces an AFTER_LOCALS lower bound.
|
||||
if version.kind == BoundaryKind.AFTER_POSTS:
|
||||
self._above = _make_above_after_posts(version.version)
|
||||
else:
|
||||
self._above = _make_above_after_locals(version.version)
|
||||
elif inclusive:
|
||||
self._above = version.__le__
|
||||
else:
|
||||
self._above = version.__lt__
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, LowerBound):
|
||||
return NotImplemented
|
||||
return self.version == other.version and self.inclusive == other.inclusive
|
||||
|
||||
def __lt__(self, other: LowerBound) -> bool:
|
||||
if not isinstance(other, LowerBound):
|
||||
return NotImplemented
|
||||
# -inf < anything (except -inf itself).
|
||||
if self.version is None:
|
||||
return other.version is not None
|
||||
if other.version is None:
|
||||
return False
|
||||
if self.version != other.version:
|
||||
return self.version < other.version
|
||||
# [v < (v: inclusive starts earlier.
|
||||
return self.inclusive and not other.inclusive
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.version, self.inclusive))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
bracket = "[" if self.inclusive else "("
|
||||
return f"<{self.__class__.__name__} {bracket}{self.version!r}>"
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class UpperBound:
|
||||
"""Upper bound of a version range.
|
||||
|
||||
A version *v* of ``None`` means unbounded above (+inf).
|
||||
At equal versions, ``v)`` sorts before ``v]`` because an exclusive
|
||||
bound ends earlier.
|
||||
"""
|
||||
|
||||
__slots__ = ("_below", "inclusive", "version")
|
||||
|
||||
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
|
||||
self.version = version
|
||||
self.inclusive = inclusive
|
||||
# Pre-bind a predicate "is parsed at or below this upper
|
||||
# bound?". See LowerBound for the rationale.
|
||||
if version is None:
|
||||
self._below: Callable[[Version], bool] | None = None
|
||||
elif isinstance(version, BoundaryVersion):
|
||||
# Standard specifiers only ever produce AFTER_LOCALS upper
|
||||
# bounds (from <=V / ==V / !=V with no local).
|
||||
if version.kind == BoundaryKind.AFTER_LOCALS:
|
||||
self._below = _make_below_after_locals(version.version)
|
||||
else:
|
||||
# An AFTER_POSTS upper is not produced by any specifier, but
|
||||
# range algebra reaches it: complementing ``>V`` flips the
|
||||
# ``AFTER_POSTS(V)`` lower into this upper bound.
|
||||
self._below = version.__ge__
|
||||
elif inclusive:
|
||||
self._below = version.__ge__
|
||||
else:
|
||||
self._below = version.__gt__
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, UpperBound):
|
||||
return NotImplemented
|
||||
return self.version == other.version and self.inclusive == other.inclusive
|
||||
|
||||
def __lt__(self, other: UpperBound) -> bool:
|
||||
if not isinstance(other, UpperBound):
|
||||
return NotImplemented
|
||||
# Nothing < +inf (except +inf itself).
|
||||
if self.version is None:
|
||||
return False
|
||||
if other.version is None:
|
||||
return True
|
||||
if self.version != other.version:
|
||||
return self.version < other.version
|
||||
# v) < v]: exclusive ends earlier.
|
||||
return not self.inclusive and other.inclusive
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.version, self.inclusive))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
bracket = "]" if self.inclusive else ")"
|
||||
return f"<{self.__class__.__name__} {self.version!r}{bracket}>"
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
#: A single contiguous interval as a (lower, upper) bound pair.
|
||||
Interval = tuple[LowerBound, UpperBound]
|
||||
|
||||
|
||||
NEG_INF: Final[LowerBound] = LowerBound(None, False)
|
||||
POS_INF: Final[UpperBound] = UpperBound(None, False)
|
||||
FULL_RANGE: Final[tuple[Interval]] = ((NEG_INF, POS_INF),)
|
||||
|
||||
|
||||
def trim_release(release: tuple[int, ...]) -> tuple[int, ...]:
|
||||
"""Strip trailing zeros from a release tuple for normalized comparison."""
|
||||
end = len(release)
|
||||
while end > 1 and release[end - 1] == 0:
|
||||
end -= 1
|
||||
return release if end == len(release) else release[:end]
|
||||
|
||||
|
||||
def _next_prefix_dev0(version: Version) -> Version:
|
||||
"""Smallest version in the next prefix: 1.2 -> 1.3.dev0."""
|
||||
release = (*version.release[:-1], version.release[-1] + 1)
|
||||
return Version.from_parts(epoch=version.epoch, release=release, dev=0)
|
||||
|
||||
|
||||
def _base_dev0(version: Version) -> Version:
|
||||
"""The .dev0 of a version's base release: 1.2 -> 1.2.dev0."""
|
||||
return Version.from_parts(epoch=version.epoch, release=version.release, dev=0)
|
||||
|
||||
|
||||
def coerce_version(version: Version | str) -> Version | None:
|
||||
if not isinstance(version, Version):
|
||||
try:
|
||||
version = Version(version)
|
||||
except InvalidVersion:
|
||||
return None
|
||||
return version
|
||||
|
||||
|
||||
def _make_above_after_posts(version: Version) -> Callable[[Version], bool]:
|
||||
"""Predicate ``parsed > AFTER_POSTS(V)`` for a lower bound.
|
||||
|
||||
Per PEP 440, ``>V`` excludes V's post-releases unless V is itself
|
||||
a post-release. AFTER_POSTS sits above V and every V.postN (with
|
||||
or without local), and just below the next release.
|
||||
"""
|
||||
version_ge = version.__ge__
|
||||
version_epoch = version.epoch
|
||||
version_pre = version.pre
|
||||
version_release_trimmed = trim_release(version.release)
|
||||
trimmed_length = len(version_release_trimmed)
|
||||
|
||||
def above(parsed: Version) -> bool:
|
||||
if version_ge(parsed):
|
||||
return False
|
||||
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
|
||||
# post family.
|
||||
if parsed.epoch != version_epoch:
|
||||
return True
|
||||
parsed_release = parsed.release
|
||||
if len(parsed_release) < trimmed_length:
|
||||
return True
|
||||
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||
return True
|
||||
for i in range(trimmed_length, len(parsed_release)):
|
||||
if parsed_release[i] != 0:
|
||||
return True
|
||||
if parsed.pre != version_pre:
|
||||
return True
|
||||
|
||||
# Same release and pre as V: parsed is in V's post family (V itself,
|
||||
# V+local, or V.postN), which the boundary sits above. A V.devN
|
||||
# (different dev, no post) sorts before V and was already caught by
|
||||
# ``version_ge`` above, so the answer here is always "not above".
|
||||
return False
|
||||
|
||||
return above
|
||||
|
||||
|
||||
def _make_above_after_locals(version: Version) -> Callable[[Version], bool]:
|
||||
"""Predicate ``parsed > AFTER_LOCALS(V)`` for a lower bound.
|
||||
|
||||
Used by the upper-side range of ``!=V`` (when V has no local
|
||||
segment). AFTER_LOCALS sits above V and every ``V+local`` but
|
||||
just below ``V.post0``.
|
||||
"""
|
||||
version_ge = version.__ge__
|
||||
version_epoch = version.epoch
|
||||
version_pre = version.pre
|
||||
version_post = version.post
|
||||
version_dev = version.dev
|
||||
version_release_trimmed = trim_release(version.release)
|
||||
trimmed_length = len(version_release_trimmed)
|
||||
|
||||
def above(parsed: Version) -> bool:
|
||||
if version_ge(parsed):
|
||||
return False
|
||||
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
|
||||
# local family (same public version, any local segment).
|
||||
if parsed.epoch != version_epoch:
|
||||
return True
|
||||
parsed_release = parsed.release
|
||||
if len(parsed_release) < trimmed_length:
|
||||
return True
|
||||
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||
return True
|
||||
for i in range(trimmed_length, len(parsed_release)):
|
||||
if parsed_release[i] != 0:
|
||||
return True
|
||||
if parsed.pre != version_pre:
|
||||
return True
|
||||
if parsed.post != version_post:
|
||||
return True
|
||||
return parsed.dev != version_dev
|
||||
|
||||
return above
|
||||
|
||||
|
||||
def _make_below_after_locals(version: Version) -> Callable[[Version], bool]:
|
||||
"""Predicate ``parsed <= AFTER_LOCALS(V)`` for an upper bound.
|
||||
|
||||
Used by ``<=V``, ``==V``, ``!=V`` (no local). ``parsed`` is at or
|
||||
below the boundary when it is at or below V cmpkey-wise, or when
|
||||
it is in V's local family.
|
||||
"""
|
||||
version_ge = version.__ge__
|
||||
version_epoch = version.epoch
|
||||
version_pre = version.pre
|
||||
version_post = version.post
|
||||
version_dev = version.dev
|
||||
version_release_trimmed = trim_release(version.release)
|
||||
trimmed_length = len(version_release_trimmed)
|
||||
|
||||
def below(parsed: Version) -> bool:
|
||||
if version_ge(parsed):
|
||||
return True
|
||||
# parsed > V cmpkey-wise: below the boundary iff in V's local
|
||||
# family.
|
||||
if parsed.epoch != version_epoch:
|
||||
return False
|
||||
parsed_release = parsed.release
|
||||
if len(parsed_release) < trimmed_length:
|
||||
return False
|
||||
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||
return False
|
||||
for i in range(trimmed_length, len(parsed_release)):
|
||||
if parsed_release[i] != 0:
|
||||
return False
|
||||
if parsed.pre != version_pre:
|
||||
return False
|
||||
if parsed.post != version_post:
|
||||
return False
|
||||
return parsed.dev == version_dev
|
||||
|
||||
return below
|
||||
|
||||
|
||||
def least_version_above(boundary: BoundaryVersion) -> Version | None:
|
||||
"""Smallest real version strictly above *boundary*, or ``None`` if none exists."""
|
||||
base = boundary.version
|
||||
|
||||
if boundary.kind == BoundaryKind.AFTER_LOCALS:
|
||||
# AFTER_LOCALS(V) sits just below V.post0, so its least successor is
|
||||
# V.post0.dev0 (V.dev(N+1) if V has a dev, V.post(N+1).dev0 if a post).
|
||||
if base.dev is not None:
|
||||
return base.__replace__(dev=base.dev + 1, local=None)
|
||||
next_post = (base.post + 1) if base.post is not None else 0
|
||||
return base.__replace__(post=next_post, dev=0, local=None)
|
||||
|
||||
# AFTER_POSTS(V): a pre-release V steps to the next pre-release's .dev0;
|
||||
# a final-release AFTER_POSTS has no least successor.
|
||||
if base.pre is not None:
|
||||
kind, number = base.pre
|
||||
return base.__replace__(pre=(kind, number + 1), post=None, dev=0, local=None)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def range_is_empty(lower: LowerBound, upper: UpperBound) -> bool:
|
||||
"""True when the range defined by *lower* and *upper* contains no versions.
|
||||
|
||||
A boundary lower sits just below the next real version, so an ordered pair
|
||||
is still empty when the upper excludes that least successor:
|
||||
``(AFTER_POSTS(1.0a1), 1.0a2.dev0)`` holds no version.
|
||||
"""
|
||||
if upper.version is None:
|
||||
return False
|
||||
|
||||
if lower.version is None:
|
||||
# Nothing sorts below MIN_VERSION, so an exclusive upper at or below it
|
||||
# leaves an empty floor interval such as ``(-inf, 0.dev0)``.
|
||||
return (
|
||||
not upper.inclusive
|
||||
and isinstance(upper.version, Version)
|
||||
and upper.version <= MIN_VERSION
|
||||
)
|
||||
|
||||
if isinstance(lower.version, BoundaryVersion):
|
||||
successor = least_version_above(lower.version)
|
||||
if successor is not None:
|
||||
if upper.version == successor:
|
||||
return not upper.inclusive
|
||||
return upper.version < successor
|
||||
|
||||
if lower.version == upper.version:
|
||||
return not (lower.inclusive and upper.inclusive)
|
||||
|
||||
return lower.version > upper.version
|
||||
|
||||
|
||||
def intersect_ranges(
|
||||
left: Sequence[Interval],
|
||||
right: Sequence[Interval],
|
||||
) -> list[Interval]:
|
||||
"""Intersect two sorted, non-overlapping range lists (two-pointer merge)."""
|
||||
result: list[Interval] = []
|
||||
left_index = right_index = 0
|
||||
while left_index < len(left) and right_index < len(right):
|
||||
left_lower, left_upper = left[left_index]
|
||||
right_lower, right_upper = right[right_index]
|
||||
|
||||
lower = max(left_lower, right_lower)
|
||||
upper = min(left_upper, right_upper)
|
||||
|
||||
if not range_is_empty(lower, upper):
|
||||
result.append((lower, upper))
|
||||
|
||||
# Advance whichever side has the smaller upper bound.
|
||||
if left_upper < right_upper:
|
||||
left_index += 1
|
||||
else:
|
||||
right_index += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def filter_by_ranges(
|
||||
ranges: Sequence[Interval],
|
||||
iterable: Iterable[Any],
|
||||
key: Callable[[Any], Version | str] | None,
|
||||
prereleases: bool | None,
|
||||
region: Sequence[Interval] = (),
|
||||
) -> Iterator[Any]:
|
||||
"""Filter *iterable* against precomputed version *ranges*.
|
||||
|
||||
With ``prereleases=None``, the PEP 440 default applies: pre-releases are
|
||||
excluded unless no final matches, in which case buffered pre-releases come
|
||||
out at the end. A pre-release inside the opt-in ``region`` is the exception:
|
||||
it is force-admitted in place, as ``prereleases=True`` would yield it. A
|
||||
force-admitted pre-release is not a final, so it never suppresses the buffer.
|
||||
"""
|
||||
if prereleases is None:
|
||||
prerelease_buffer: list[Any] = []
|
||||
found_final = False
|
||||
|
||||
if len(ranges) == 1:
|
||||
# Hot path: most specifiers and small SpecifierSets reduce to
|
||||
# a single contiguous range.
|
||||
lower, upper = ranges[0]
|
||||
above = lower._above
|
||||
below = upper._below
|
||||
for item in iterable:
|
||||
parsed = coerce_version(item if key is None else key(item))
|
||||
if parsed is None:
|
||||
continue
|
||||
if above is not None and not above(parsed):
|
||||
continue
|
||||
if below is not None and not below(parsed):
|
||||
continue
|
||||
if not parsed.is_prerelease:
|
||||
found_final = True
|
||||
yield item
|
||||
elif region and matches_bounds_only(region, parsed):
|
||||
yield item
|
||||
elif not found_final:
|
||||
prerelease_buffer.append(item)
|
||||
if not found_final:
|
||||
yield from prerelease_buffer
|
||||
return
|
||||
|
||||
for item in iterable:
|
||||
parsed = coerce_version(item if key is None else key(item))
|
||||
if parsed is None:
|
||||
continue
|
||||
for lower, upper in ranges:
|
||||
above = lower._above
|
||||
if above is not None and not above(parsed):
|
||||
break
|
||||
below = upper._below
|
||||
if below is None or below(parsed):
|
||||
if not parsed.is_prerelease:
|
||||
found_final = True
|
||||
yield item
|
||||
elif region and matches_bounds_only(region, parsed):
|
||||
yield item
|
||||
elif not found_final:
|
||||
prerelease_buffer.append(item)
|
||||
break
|
||||
if not found_final:
|
||||
yield from prerelease_buffer
|
||||
return
|
||||
|
||||
exclude_prereleases = prereleases is False
|
||||
|
||||
if len(ranges) == 1:
|
||||
# Hot path: most specifiers and small SpecifierSets reduce to
|
||||
# a single contiguous range.
|
||||
lower, upper = ranges[0]
|
||||
above = lower._above
|
||||
below = upper._below
|
||||
for item in iterable:
|
||||
parsed = coerce_version(item if key is None else key(item))
|
||||
if parsed is None:
|
||||
continue
|
||||
if exclude_prereleases and parsed.is_prerelease:
|
||||
continue
|
||||
if above is not None and not above(parsed):
|
||||
continue
|
||||
if below is None or below(parsed):
|
||||
yield item
|
||||
return
|
||||
|
||||
for item in iterable:
|
||||
parsed = coerce_version(item if key is None else key(item))
|
||||
if parsed is None:
|
||||
continue
|
||||
if exclude_prereleases and parsed.is_prerelease:
|
||||
continue
|
||||
for lower, upper in ranges:
|
||||
above = lower._above
|
||||
if above is not None and not above(parsed):
|
||||
break
|
||||
below = upper._below
|
||||
if below is None or below(parsed):
|
||||
yield item
|
||||
break
|
||||
|
||||
|
||||
def _nearest_release_above_prerelease(version: Version) -> Version:
|
||||
"""Smallest non-pre-release at or above a pre-release *version*."""
|
||||
if version.pre is not None:
|
||||
# An a/b/rc pre-release drops to its final release, which outranks
|
||||
# every post-release of that pre-release (1.0a1.post0 -> 1.0).
|
||||
return version.__replace__(pre=None, post=None, dev=None, local=None)
|
||||
|
||||
# A dev-only release keeps its post-release (1.0.post0.dev0 -> 1.0.post0,
|
||||
# whose final 1.0 sorts below it).
|
||||
return version.__replace__(dev=None, local=None)
|
||||
|
||||
|
||||
def _lowest_release_at_or_above(value: Version | BoundaryVersion | None) -> Version:
|
||||
"""Smallest non-pre-release version at or above *value*.
|
||||
|
||||
``None`` is the ``-inf`` floor, whose nearest non-pre-release is
|
||||
:data:`MIN_RELEASE`.
|
||||
"""
|
||||
if value is None:
|
||||
return MIN_RELEASE
|
||||
if isinstance(value, BoundaryVersion):
|
||||
inner_version = value.version
|
||||
if inner_version.is_prerelease:
|
||||
return _nearest_release_above_prerelease(inner_version)
|
||||
# AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0
|
||||
# AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1
|
||||
next_post = (inner_version.post + 1) if inner_version.post is not None else 0
|
||||
return inner_version.__replace__(post=next_post, local=None)
|
||||
|
||||
if not value.is_prerelease:
|
||||
return value
|
||||
|
||||
return _nearest_release_above_prerelease(value)
|
||||
|
||||
|
||||
def ranges_are_prerelease_only(ranges: Sequence[Interval]) -> bool:
|
||||
"""True when every range in *ranges* contains only pre-releases.
|
||||
|
||||
Used to detect unsatisfiable specifier sets when ``prereleases=False``:
|
||||
if every range is pre-release-only, every contained version is excluded.
|
||||
"""
|
||||
for lower, upper in ranges:
|
||||
nearest = _lowest_release_at_or_above(lower.version)
|
||||
if upper.version is None or nearest < upper.version:
|
||||
return False
|
||||
if nearest == upper.version and upper.inclusive:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def wildcard_ranges(op: str, base: Version) -> list[Interval]:
|
||||
"""Ranges for ==V.* and !=V.*.
|
||||
|
||||
==1.2.* -> [1.2.dev0, 1.3.dev0); !=1.2.* -> complement.
|
||||
"""
|
||||
lower = _base_dev0(base)
|
||||
upper = _next_prefix_dev0(base)
|
||||
if op == "==":
|
||||
return [(LowerBound(lower, True), UpperBound(upper, False))]
|
||||
# !=
|
||||
return [
|
||||
(NEG_INF, UpperBound(lower, False)),
|
||||
(LowerBound(upper, True), POS_INF),
|
||||
]
|
||||
|
||||
|
||||
def standard_ranges(op: str, version: Version, has_local: bool) -> list[Interval]:
|
||||
"""Ranges for the standard PEP 440 operators (no wildcard, no ===).
|
||||
|
||||
*has_local* indicates whether the spec string included a ``+local``
|
||||
segment; relevant only for ``==`` / ``!=`` to decide whether the
|
||||
upper bound includes V's local family.
|
||||
"""
|
||||
if op == ">=":
|
||||
return [(LowerBound(version, True), POS_INF)]
|
||||
|
||||
if op == "<=":
|
||||
return [
|
||||
(
|
||||
NEG_INF,
|
||||
UpperBound(BoundaryVersion(version, BoundaryKind.AFTER_LOCALS), True),
|
||||
)
|
||||
]
|
||||
|
||||
if op == ">":
|
||||
if version.dev is not None:
|
||||
# >V.devN: dev versions have no post-releases, so the
|
||||
# next real version is V.dev(N+1).
|
||||
lower_bound = version.__replace__(dev=version.dev + 1, local=None)
|
||||
return [(LowerBound(lower_bound, True), POS_INF)]
|
||||
if version.post is not None:
|
||||
# >V.postN: next real version is V.post(N+1).dev0.
|
||||
lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None)
|
||||
return [(LowerBound(lower_bound, True), POS_INF)]
|
||||
# >V (final or pre-release V): exclude V itself, V+local, and
|
||||
# every V.postN per PEP 440.
|
||||
return [
|
||||
(
|
||||
LowerBound(BoundaryVersion(version, BoundaryKind.AFTER_POSTS), False),
|
||||
POS_INF,
|
||||
)
|
||||
]
|
||||
|
||||
if op == "<":
|
||||
# <V excludes pre-releases of V when V is not a pre-release.
|
||||
# V.dev0 is the earliest pre-release of V.
|
||||
bound = (
|
||||
version if version.is_prerelease else version.__replace__(dev=0, local=None)
|
||||
)
|
||||
if bound <= MIN_VERSION:
|
||||
return []
|
||||
return [(NEG_INF, UpperBound(bound, False))]
|
||||
|
||||
# ==, !=: local versions of V match when the spec has no local segment.
|
||||
after_locals = BoundaryVersion(version, BoundaryKind.AFTER_LOCALS)
|
||||
upper = version if has_local else after_locals
|
||||
|
||||
if op == "==":
|
||||
return [(LowerBound(version, True), UpperBound(upper, True))]
|
||||
|
||||
if op == "!=":
|
||||
return [
|
||||
(NEG_INF, UpperBound(version, False)),
|
||||
(LowerBound(upper, False), POS_INF),
|
||||
]
|
||||
|
||||
if op == "~=":
|
||||
prefix = version.__replace__(release=version.release[:-1])
|
||||
return [
|
||||
(LowerBound(version, True), UpperBound(_next_prefix_dev0(prefix), False))
|
||||
]
|
||||
|
||||
raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover
|
||||
|
||||
|
||||
def bounds_for_spec(op: str, version_str: str, version: Version) -> list[Interval]:
|
||||
"""Ranges for one specifier's ``(op, version_str)``.
|
||||
|
||||
Dispatches between the wildcard and standard builders. ``version`` is the
|
||||
parsed ``version_str`` (its base, without the trailing ``.*``, for
|
||||
wildcards). ``===`` is not handled here; its match is a literal string
|
||||
compared in :mod:`packaging.specifiers`.
|
||||
"""
|
||||
if version_str.endswith(".*"):
|
||||
return wildcard_ranges(op, version)
|
||||
|
||||
return standard_ranges(op, version, "+" in version_str)
|
||||
|
||||
|
||||
def intersect_specifier_bounds(
|
||||
per_specifier_ranges: Iterable[Sequence[Interval]],
|
||||
) -> Sequence[Interval]:
|
||||
"""Intersect each specifier's ranges into a single sequence.
|
||||
|
||||
Short-circuits once the running intersection is empty, since no later
|
||||
specifier can revive it. Callers must pass at least one specifier.
|
||||
"""
|
||||
result: Sequence[Interval] | None = None
|
||||
for sub in per_specifier_ranges:
|
||||
if result is None:
|
||||
result = sub
|
||||
else:
|
||||
result = intersect_ranges(result, sub)
|
||||
if not result:
|
||||
break
|
||||
|
||||
if result is None: # pragma: no cover - callers guard non-empty input
|
||||
raise RuntimeError("intersect_specifier_bounds called with no specifiers")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def matches_bounds_only(ranges: Sequence[Interval], version: Version) -> bool:
|
||||
"""Whether ``version`` falls within any of ``ranges``.
|
||||
|
||||
The pure bounds membership test, for a single already-parsed version with
|
||||
no pre-release policy applied. ``ranges`` are sorted and non-overlapping,
|
||||
so a version below one range's lower bound is below every later range too.
|
||||
"""
|
||||
for lower, upper in ranges:
|
||||
above = lower._above
|
||||
if above is not None and not above(version):
|
||||
return False
|
||||
|
||||
below = upper._below
|
||||
if below is None or below(version):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def resolve_prereleases(
|
||||
configured: bool | None, autodetected: bool | None
|
||||
) -> bool | None:
|
||||
"""Resolve a specifier's effective default pre-release policy.
|
||||
|
||||
An explicit ``configured`` value wins; otherwise an autodetected ``True``
|
||||
propagates and anything else falls back to the PEP 440 default (``None``).
|
||||
"""
|
||||
if configured is not None:
|
||||
return configured
|
||||
|
||||
if autodetected:
|
||||
return True
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user