| |
| |
| |
| """Public :class:`VersionRange` API. |
| |
| A set-algebra view of the versions accepted by a |
| :class:`~packaging.specifiers.SpecifierSet`. Ranges support intersection, |
| union, complement, and difference; membership and filtering match the |
| originating specifier set; and conversion back to a |
| :class:`~packaging.specifiers.SpecifierSet` is available where a PEP 440 form |
| exists. |
| |
| .. testsetup:: |
| |
| from packaging.ranges import VersionRange |
| from packaging.specifiers import SpecifierSet |
| from packaging.version import Version |
| """ |
|
|
| from __future__ import annotations |
|
|
| import enum |
| import typing |
| from typing import ( |
| TYPE_CHECKING, |
| Any, |
| TypeVar, |
| Union, |
| ) |
|
|
| from ._ranges import ( |
| FULL_RANGE, |
| MIN_VERSION, |
| NEG_INF, |
| POS_INF, |
| BoundaryKind, |
| BoundaryVersion, |
| LowerBound, |
| UpperBound, |
| coerce_version, |
| filter_by_ranges, |
| intersect_ranges, |
| least_version_above, |
| matches_bounds_only, |
| range_is_empty, |
| ranges_are_prerelease_only, |
| trim_release, |
| ) |
| from .version import Version |
|
|
| if TYPE_CHECKING: |
| from collections.abc import Callable, Iterable, Iterator, Sequence |
|
|
| from ._ranges import Interval |
| from .specifiers import SpecifierSet |
|
|
|
|
| __all__ = ["VersionRange"] |
|
|
| T = TypeVar("T") |
| UnparsedVersion = Union[Version, str] |
| UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| _MAX_EXCLUSION_RUN = 128 |
|
|
|
|
| class _SetOp(enum.Enum): |
| """The binary set operation ``_combine_literals`` resolves over ``===`` literals.""" |
|
|
| INTERSECTION = enum.auto() |
| UNION = enum.auto() |
| DIFFERENCE = enum.auto() |
|
|
|
|
| def __dir__() -> list[str]: |
| return __all__ |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _union_ranges( |
| left: Sequence[Interval], |
| right: Sequence[Interval], |
| ) -> list[Interval]: |
| """Union two sorted, non-overlapping interval lists. |
| |
| A linear merge over the two pre-sorted inputs followed by a single |
| coalescing pass: adjacent or overlapping intervals collapse so the result |
| is itself sorted and non-overlapping. |
| """ |
| if not left: |
| return list(right) |
| if not right: |
| return list(left) |
|
|
| merged_input: list[Interval] = [] |
| left_index = right_index = 0 |
| while left_index < len(left) and right_index < len(right): |
| if left[left_index][0] <= right[right_index][0]: |
| merged_input.append(left[left_index]) |
| left_index += 1 |
| else: |
| merged_input.append(right[right_index]) |
| right_index += 1 |
| merged_input.extend(left[left_index:]) |
| merged_input.extend(right[right_index:]) |
|
|
| merged: list[Interval] = [merged_input[0]] |
| for lower, upper in merged_input[1:]: |
| prev_lower, prev_upper = merged[-1] |
|
|
| if ( |
| prev_upper.version is None |
| or lower.version is None |
| or prev_upper.version > lower.version |
| ): |
| overlaps = True |
| elif prev_upper.version == lower.version: |
| overlaps = prev_upper.inclusive or lower.inclusive |
| else: |
| |
| |
| |
| gap_lower = LowerBound(prev_upper.version, not prev_upper.inclusive) |
| gap_upper = UpperBound(lower.version, not lower.inclusive) |
| overlaps = range_is_empty(gap_lower, gap_upper) |
|
|
| if overlaps: |
| merged[-1] = (prev_lower, max(prev_upper, upper)) |
| else: |
| merged.append((lower, upper)) |
|
|
| return merged |
|
|
|
|
| def _complement_ranges(ranges: Sequence[Interval]) -> list[Interval]: |
| """Complement a sorted, non-overlapping interval list. |
| |
| Yields the gaps between intervals plus a leading gap before the first and |
| a trailing gap after the last. Bound inclusivity flips so that |
| complement-of-complement round-trips back to the input. |
| """ |
| if not ranges: |
| return list(FULL_RANGE) |
|
|
| result: list[Interval] = [] |
| prev_upper: UpperBound | None = None |
|
|
| for lower, upper in ranges: |
| if prev_upper is None: |
| |
| |
| |
| |
| |
| if lower.version is not None: |
| gap_upper = UpperBound(lower.version, not lower.inclusive) |
| result.append((NEG_INF, gap_upper)) |
| else: |
| gap_lower = LowerBound(prev_upper.version, not prev_upper.inclusive) |
| gap_upper = UpperBound(lower.version, not lower.inclusive) |
| |
| |
| result.append((gap_lower, gap_upper)) |
| prev_upper = upper |
|
|
| |
| assert prev_upper is not None |
| if prev_upper.version is not None: |
| gap_lower = LowerBound(prev_upper.version, not prev_upper.inclusive) |
| result.append((gap_lower, POS_INF)) |
|
|
| return result |
|
|
|
|
| def _canonical_floor(bounds: tuple[Interval, ...]) -> tuple[Interval, ...]: |
| """Collapse the PEP 440 floor in a sorted interval list. |
| |
| Only the first interval can touch ``0.dev0`` (the minimum version). An |
| inclusive lower at or below it admits everything below, the same as |
| ``-inf``, so ``>=0.dev0`` becomes the one canonical full range. An |
| exclusive upper at or below it leaves the interval empty, so it is dropped. |
| """ |
| if not bounds: |
| return bounds |
|
|
| lower, upper = bounds[0] |
| if range_is_empty(NEG_INF, upper): |
| return bounds[1:] |
|
|
| if ( |
| lower.inclusive |
| and isinstance(lower.version, Version) |
| and lower.version <= MIN_VERSION |
| ): |
| return ((NEG_INF, upper), *bounds[1:]) |
|
|
| return bounds |
|
|
|
|
| def _predecessor_boundary(version: Version) -> BoundaryVersion | None: |
| """The boundary whose least successor is *version*, or ``None``. |
| |
| Inverse of :func:`~packaging._ranges.least_version_above`. A plain version |
| that is exactly such a successor (``1.0a2.dev0`` sits just above |
| ``AFTER_POSTS(1.0a1)``) folds back to that boundary, so ``>=1.0a2.dev0`` and |
| ``>1.0a1`` share one form. The proposed boundary is confirmed by |
| round-tripping through ``least_version_above``. |
| """ |
| |
| if version.dev is None: |
| return None |
|
|
| candidate: BoundaryVersion | None = None |
| if version.pre is not None and version.dev == 0 and version.post is None: |
| |
| kind, number = version.pre |
| if number >= 1: |
| candidate = BoundaryVersion( |
| version.__replace__(pre=(kind, number - 1), dev=None), |
| BoundaryKind.AFTER_POSTS, |
| ) |
| elif version.dev >= 1: |
| |
| candidate = BoundaryVersion( |
| version.__replace__(dev=version.dev - 1), BoundaryKind.AFTER_LOCALS |
| ) |
| elif version.dev == 0 and version.post is not None: |
| |
| base = ( |
| version.__replace__(post=None, dev=None) |
| if version.post == 0 |
| else version.__replace__(post=version.post - 1, dev=None) |
| ) |
| candidate = BoundaryVersion(base, BoundaryKind.AFTER_LOCALS) |
|
|
| if candidate is not None and least_version_above(candidate) == version: |
| return candidate |
| return None |
|
|
|
|
| def _canonicalize(bounds: tuple[Interval, ...]) -> tuple[Interval, ...]: |
| """Fold least-successor bounds to their boundary form. |
| |
| ``>=1.0a2.dev0`` and ``>1.0a1`` denote the same set, so both must reduce to |
| one representation for ``==`` and ``hash`` to agree. An inclusive lower or |
| exclusive upper sitting on a boundary's least successor becomes that |
| boundary; the engine's emptiness check has already dropped the synthetic |
| gaps such intervals would otherwise leave. |
| """ |
| result: list[Interval] = [] |
| for lower, upper in bounds: |
| new_lower, new_upper = lower, upper |
|
|
| if isinstance(lower.version, Version) and lower.inclusive: |
| boundary = _predecessor_boundary(lower.version) |
| if boundary is not None: |
| new_lower = LowerBound(boundary, inclusive=False) |
|
|
| if isinstance(upper.version, Version) and not upper.inclusive: |
| boundary = _predecessor_boundary(upper.version) |
| if boundary is not None: |
| new_upper = UpperBound(boundary, inclusive=True) |
|
|
| result.append((new_lower, new_upper)) |
| return tuple(result) |
|
|
|
|
| def _struct_admits( |
| bounds: tuple[Interval, ...], admit_arbitrary: bool, literal: str |
| ) -> bool: |
| """True when the bounds (plus arbitrary admission) admit ``literal``. |
| |
| Skips the explicit admit/reject sets, which the caller layers on top. A |
| non-version string matches via ``admit_arbitrary`` only on full bounds; |
| on narrower bounds the flag is metadata only. |
| """ |
| parsed = coerce_version(literal) |
| if parsed is None: |
| return admit_arbitrary and bounds == FULL_RANGE |
|
|
| return matches_bounds_only(bounds, parsed) |
|
|
|
|
| |
|
|
|
|
| def _bound_version_str(value: BoundaryVersion | Version) -> str: |
| """Printout for a bound's inner value, kind-tagged for boundaries.""" |
| if isinstance(value, BoundaryVersion): |
| return f"{value.version}[{value.kind.name}]" |
| return str(value) |
|
|
|
|
| def _format_lower(bound: LowerBound) -> str: |
| if bound.version is None: |
| return "(-inf" |
| bracket = "[" if bound.inclusive else "(" |
| return f"{bracket}{_bound_version_str(bound.version)}" |
|
|
|
|
| def _format_upper(bound: UpperBound) -> str: |
| if bound.version is None: |
| return "+inf)" |
| bracket = "]" if bound.inclusive else ")" |
| return f"{_bound_version_str(bound.version)}{bracket}" |
|
|
|
|
| def _format_intervals(intervals: Sequence[Interval]) -> str: |
| """Render a sorted interval list as ``lower, upper | lower, upper``.""" |
| return " | ".join( |
| f"{_format_lower(lower)}, {_format_upper(upper)}" for lower, upper in intervals |
| ) |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| def _is_dev0_version(version: Version) -> bool: |
| """True when version is exactly ``X[.Y]*.dev0`` (the shape ``<X`` makes).""" |
| return ( |
| version.dev == 0 |
| and version.pre is None |
| and version.post is None |
| and version.local is None |
| ) |
|
|
|
|
| def _clean_lower(version: Version) -> list[str] | None: |
| """A prerelease-free spelling for an inclusive ``[version`` lower, or ``None``. |
| |
| Several ``[V`` lowers come from an operator whose own spelling carries no |
| synthetic ``.dev0``. Recovering that spelling gives the range an empty opt-in |
| region, so it is offered in the prerelease-free spelling mode (see |
| :meth:`VersionRange.to_specifier_set`). |
| """ |
| if version.dev != 0 or version.pre is not None or version.local is not None: |
| return None |
|
|
| |
| if version.post is not None: |
| if version.post < 1: |
| return None |
| return [f">{version.__replace__(post=version.post - 1, dev=None)}"] |
|
|
| |
| |
| family = trim_release(version.release) |
| last = family[-1] |
| if last < 1: |
| return None |
|
|
| below_release = (*family[:-1], last - 1) |
| below = Version.from_parts(epoch=version.epoch, release=below_release) |
|
|
| |
| |
| if version.epoch == 0 and not any(below_release): |
| return [f"!={below}.*"] |
|
|
| return [f">={below}", f"!={below}.*"] |
|
|
|
|
| def _epoch_floor_lower( |
| lower: LowerBound, upper: UpperBound |
| ) -> tuple[Version, int, bool] | None: |
| """The ``E!0`` family of a lower sitting on an epoch>0 zero-family floor. |
| |
| An epoch>0 zero-family base such as ``1!0.dev0`` has no ``>=P,!=P.*`` spelling |
| since no version sorts below ``E!0`` within the epoch. While the interval |
| stays within ``==E!0.*`` it is that wildcard, trimmed by the upper and with a |
| leading ``.dev`` run excluded: an ``AFTER_LOCALS(E!0.dev(k))`` lower drops |
| ``E!0.dev0..E!0.dev(k)``, a plain inclusive ``E!0.dev0`` lower drops none. |
| Returns the ``E!0`` family, how many leading ``.dev`` releases to exclude, and |
| whether the upper sits at the family cap (so ``==E!0.*`` needs no upper), else |
| ``None``. |
| """ |
| version = lower.version |
| if isinstance(version, BoundaryVersion): |
| if version.kind != BoundaryKind.AFTER_LOCALS: |
| return None |
| version = version.version |
| if version.dev is None: |
| return None |
| excluded_devs = version.dev + 1 |
| elif isinstance(version, Version) and lower.inclusive: |
| |
| |
| if version.dev != 0: |
| return None |
| excluded_devs = 0 |
| else: |
| return None |
|
|
| |
| if version.epoch == 0: |
| return None |
| if version.pre is not None or version.post is not None or version.local is not None: |
| return None |
| if any(trim_release(version.release)): |
| return None |
|
|
| |
| next_family = Version.from_parts(epoch=version.epoch, release=(1,), dev=0) |
| cap = UpperBound(next_family, False) |
| if upper > cap: |
| return None |
|
|
| family = Version.from_parts(epoch=version.epoch, release=(0,)) |
| return family, excluded_devs, upper == cap |
|
|
|
|
| def _dev_family_anchor(family: Version) -> list[str] | None: |
| """Prerelease-free fragments for ``[family, ..)``, or ``None`` if it has none. |
| |
| ``family`` is an ``X.dev0``. The floor gives ``[]`` (every version); a release |
| base its ``_clean_lower`` family-floor spelling (``!=0.*`` ...); an ``X.post0`` |
| base ``>=X,!=X``. A pre-release base has no prerelease-free spelling. |
| """ |
| if family <= MIN_VERSION: |
| return [] |
| clean = _clean_lower(family) |
| if clean is not None: |
| return clean |
| if family.pre is None and family.post == 0: |
| base = family.__replace__(post=None, dev=None) |
| return [f">={base}", f"!={base}"] |
| return None |
|
|
|
|
| def _encode_lower(lower: LowerBound, keep_dev0: bool) -> list[str] | None: |
| """Encode a lower bound as specifier fragments, or ``None``. |
| |
| ``[]`` for ``-inf``. An ``AFTER_POSTS(V)`` lower is ``>V``. An |
| ``AFTER_LOCALS(V)`` lower is the set ``[successor, ..)`` and emits ``>=V,!=V``, |
| except in the prerelease-free spelling mode (``keep_dev0`` false), where it |
| recovers a spelling with no synthetic ``.dev0`` when one exists: |
| ``>3.8.post1`` for a post release, or a dev family's anchor plus the dev run |
| up to V for a ``.dev`` release. |
| """ |
| lower_version = lower.version |
| if lower_version is None: |
| return [] |
|
|
| if isinstance(lower_version, BoundaryVersion): |
| if lower_version.kind == BoundaryKind.AFTER_POSTS: |
| |
| return [f">{lower_version.version}"] |
| inner = lower_version.version |
| if inner <= MIN_VERSION: |
| |
| |
| return [f"!={inner}"] |
| |
| |
| |
| if not keep_dev0: |
| if inner.dev is not None: |
| |
| |
| |
| |
| family = inner.__replace__(dev=0) |
| anchor = _dev_family_anchor(family) |
| if anchor is not None: |
| if inner.dev + 1 > _MAX_EXCLUSION_RUN: |
| return None |
| run = [ |
| f"!={family.__replace__(dev=d)}" for d in range(inner.dev + 1) |
| ] |
| return anchor + run |
| else: |
| |
| |
| successor = least_version_above(lower_version) |
| clean = _clean_lower(successor) if successor is not None else None |
| if clean is not None: |
| return clean |
| |
| |
| |
| |
| return [f">={inner}", f"!={inner}"] |
|
|
| if not lower.inclusive: |
| return None |
|
|
| |
| if not keep_dev0: |
| clean = _clean_lower(lower_version) |
| if clean is not None: |
| return clean |
| return [f">={lower_version}"] |
|
|
|
|
| def _encode_upper(upper: UpperBound, keep_dev0: bool) -> list[str] | None: |
| """Encode an upper bound as specifier fragments, or ``None``. |
| |
| ``[]`` for ``+inf``. In the prerelease-free spelling mode (``keep_dev0`` |
| false) the ``<X`` spelling is used for the ``X.dev0`` upper that ``<X`` |
| builds; otherwise the synthetic ``.dev0`` is kept so the range opts its |
| bounds in. |
| """ |
| upper_version = upper.version |
| if upper_version is None: |
| return [] |
|
|
| if isinstance(upper_version, BoundaryVersion): |
| |
| |
| if upper_version.kind == BoundaryKind.AFTER_LOCALS: |
| inner = upper_version.version |
| if ( |
| not keep_dev0 |
| and inner.pre is None |
| and inner.post is not None |
| and inner.dev is None |
| ): |
| |
| |
| |
| return [f"<{inner.__replace__(post=inner.post + 1)}"] |
| return [f"<={inner}"] |
| |
| |
| |
| successor = least_version_above(upper_version) |
| if successor is not None: |
| return [f"<{successor}"] |
| return None |
|
|
| if not upper.inclusive: |
| |
| |
| |
| if ( |
| upper_version.dev == 0 |
| and upper_version.pre is None |
| and upper_version.local is None |
| ): |
| if not keep_dev0: |
| return [f"<{upper_version.__replace__(dev=None)}"] |
| return [f"<{upper_version}"] |
| |
| return [f"<={upper_version}", f"!={upper_version}"] |
| return None |
|
|
|
|
| def _detect_equal_wildcard(lower: LowerBound, upper: UpperBound) -> Version | None: |
| """If ``[lower, upper)`` is the ``==V.*`` shape, return ``V``.""" |
| if isinstance(lower.version, BoundaryVersion) or isinstance( |
| upper.version, BoundaryVersion |
| ): |
| return None |
| if lower.version is None or upper.version is None: |
| return None |
| if not lower.inclusive or upper.inclusive: |
| return None |
| if not (_is_dev0_version(lower.version) and _is_dev0_version(upper.version)): |
| return None |
| if lower.version.epoch != upper.version.epoch: |
| return None |
|
|
| lower_release = trim_release(lower.version.release) |
| upper_release = trim_release(upper.version.release) |
| padded_length = max(len(lower_release), len(upper_release)) |
| assert padded_length > 0 |
| lower_release += (0,) * (padded_length - len(lower_release)) |
| upper_release += (0,) * (padded_length - len(upper_release)) |
|
|
| if lower_release[:-1] != upper_release[:-1]: |
| return None |
|
|
| |
| |
| |
| if upper_release[-1] != lower_release[-1] + 1: |
| return None |
|
|
| return lower.version.__replace__(release=lower_release, dev=None) |
|
|
|
|
| def _encode_interval( |
| lower: LowerBound, upper: UpperBound, keep_dev0: bool |
| ) -> list[str] | None: |
| """Encode one interval as specifier fragments, or ``None``. |
| |
| Special-cases the ``==V`` singleton (``[V, AFTER_LOCALS(V)]`` for a plain |
| ``V``, and ``[V+local, V+local]`` for a local one) and the ``==V.*`` shape |
| so the fragment is one equality rather than a bound pair. |
| """ |
| |
| if ( |
| lower.version is not None |
| and upper.version is not None |
| and not isinstance(lower.version, BoundaryVersion) |
| and not isinstance(upper.version, BoundaryVersion) |
| and lower.inclusive |
| and upper.inclusive |
| and lower.version == upper.version |
| and lower.version.local is not None |
| ): |
| return [f"=={lower.version}"] |
|
|
| |
| |
| if ( |
| isinstance(lower.version, Version) |
| and lower.inclusive |
| and upper.inclusive |
| and isinstance(upper.version, BoundaryVersion) |
| and upper.version.kind == BoundaryKind.AFTER_LOCALS |
| and upper.version.version == lower.version |
| ): |
| return [f"=={lower.version}"] |
|
|
| wildcard = _detect_equal_wildcard(lower, upper) |
| if wildcard is not None: |
| return [f"=={wildcard}.*"] |
|
|
| |
| |
| floor = _epoch_floor_lower(lower, upper) if not keep_dev0 else None |
| if floor is not None: |
| family, excluded_devs, upper_at_cap = floor |
| if excluded_devs > _MAX_EXCLUSION_RUN: |
| return None |
| parts = [f"=={family}.*"] |
| parts.extend(f"!={family.__replace__(dev=d)}" for d in range(excluded_devs)) |
|
|
| |
| if not upper_at_cap: |
| upper_parts = _encode_upper(upper, keep_dev0) |
| if upper_parts is None: |
| return None |
| parts.extend(upper_parts) |
|
|
| return parts |
|
|
| lower_parts = _encode_lower(lower, keep_dev0) |
| if lower_parts is None: |
| return None |
|
|
| upper_parts = _encode_upper(upper, keep_dev0) |
| if upper_parts is None: |
| return None |
|
|
| return lower_parts + upper_parts |
|
|
|
|
| |
|
|
|
|
| def _detect_not_equal( |
| left_upper: UpperBound, right_lower: LowerBound |
| ) -> list[Version] | None: |
| """If the gap between two intervals is a ``!=V`` chain, list its points. |
| |
| A plain exclusive left upper names the first excluded V directly; an inclusive |
| boundary left upper names it via its least successor. Adjacent exclusions |
| (``V`` and its immediate successors) share a single gap spanning a contiguous |
| dev run, so one gap can name a short chain: ``!=1.0,!=1.0.post0.dev0`` is one |
| gap from ``1.0`` up to ``AFTER_LOCALS(1.0.post0.dev0)``. |
| """ |
| if isinstance(left_upper.version, BoundaryVersion): |
| |
| |
| first = least_version_above(left_upper.version) |
| if first is None: |
| return None |
| elif left_upper.version is None or left_upper.inclusive: |
| return None |
| else: |
| first = left_upper.version |
|
|
| if not isinstance(right_lower.version, BoundaryVersion): |
| if ( |
| right_lower.version is not None |
| and not right_lower.inclusive |
| and right_lower.version == first |
| and first.local is not None |
| ): |
| return [first] |
| return None |
|
|
| if right_lower.version.kind != BoundaryKind.AFTER_LOCALS: |
| return None |
|
|
| |
| last = right_lower.version.version |
| if first == last: |
| return [first] |
|
|
| |
| |
| |
| |
| |
| second = least_version_above(BoundaryVersion(first, BoundaryKind.AFTER_LOCALS)) |
| if ( |
| second is not None |
| and second.dev is not None |
| and last.dev is not None |
| and last.dev >= second.dev |
| and last.__replace__(dev=second.dev) == second |
| ): |
| |
| if last.dev - second.dev + 2 > _MAX_EXCLUSION_RUN: |
| return None |
| run = (second.__replace__(dev=d) for d in range(second.dev, last.dev + 1)) |
| return [first, *run] |
| return None |
|
|
|
|
| def _decompose_dev0_gap( |
| lower_trim: tuple[int, ...], |
| upper_trim: tuple[int, ...], |
| epoch: int, |
| budget: int = _MAX_EXCLUSION_RUN, |
| ) -> list[Version] | None: |
| """Decompose the gap ``[L.dev0, U.dev0)`` into wildcard prefixes. |
| |
| ``lower_trim``/``upper_trim`` are trimmed release tuples with |
| ``lower_trim < upper_trim`` lexicographically. The chain sweeps at the |
| first differing level. The gap is undecomposable when L has trailing |
| components below that level (the chain cannot escape L's subtree), or when |
| the chain, summed across levels, would exceed ``budget`` prefixes. |
| """ |
| diff = 0 |
| while ( |
| diff < len(lower_trim) |
| and diff < len(upper_trim) |
| and lower_trim[diff] == upper_trim[diff] |
| ): |
| diff += 1 |
|
|
| if len(lower_trim) > diff + 1: |
| return None |
|
|
| common = lower_trim[:diff] |
| lower_val = lower_trim[diff] if len(lower_trim) > diff else 0 |
| upper_val = upper_trim[diff] |
|
|
| span = upper_val - lower_val |
| if span > budget: |
| return None |
|
|
| fragments = [ |
| Version.from_parts(epoch=epoch, release=(*common, segment)) |
| for segment in range(lower_val, upper_val) |
| ] |
|
|
| if len(upper_trim) == diff + 1: |
| return fragments |
|
|
| |
| |
| |
| |
| tail = _decompose_dev0_gap( |
| (*common, upper_val), upper_trim, epoch, budget - max(span, 1) |
| ) |
| if tail is None: |
| return None |
| return fragments + tail |
|
|
|
|
| def _encode_gap(left_upper: UpperBound, right_lower: LowerBound) -> list[str] | None: |
| """Encode the gap between two adjacent intervals as ``!=`` fragments. |
| |
| A point chain becomes ``!=V`` fragments and a dev0 family span becomes |
| ``!=P.*`` prefixes, followed by a leading dev run in the last family when |
| the gap ends inside it. Any other gap has no exclusion form and returns |
| ``None``. |
| """ |
| |
| |
| points = _detect_not_equal(left_upper, right_lower) |
| if points is not None: |
| return [f"!={point}" for point in points] |
|
|
| |
| left_v = left_upper.version |
| if ( |
| not isinstance(left_v, Version) |
| or left_upper.inclusive |
| or not _is_dev0_version(left_v) |
| ): |
| return None |
|
|
| right_v = right_lower.version |
| if isinstance(right_v, Version) and right_lower.inclusive: |
| |
| upper_dev0 = right_v |
| run_length = 0 |
| budget = _MAX_EXCLUSION_RUN |
| elif ( |
| isinstance(right_v, BoundaryVersion) |
| and right_v.kind == BoundaryKind.AFTER_LOCALS |
| and not right_lower.inclusive |
| ): |
| |
| |
| |
| upper = right_v.version |
| if upper.dev is None or upper.dev + 1 > _MAX_EXCLUSION_RUN: |
| return None |
| upper_dev0 = upper.__replace__(dev=0) |
| run_length = upper.dev + 1 |
| budget = _MAX_EXCLUSION_RUN - run_length |
| else: |
| return None |
|
|
| |
| |
| if not _is_dev0_version(upper_dev0): |
| return None |
| if left_v.epoch != upper_dev0.epoch or left_v >= upper_dev0: |
| return None |
|
|
| prefixes = _decompose_dev0_gap( |
| trim_release(left_v.release), |
| trim_release(upper_dev0.release), |
| left_v.epoch, |
| budget, |
| ) |
| if prefixes is None: |
| return None |
|
|
| exclusions = [f"!={prefix}.*" for prefix in prefixes] |
| exclusions.extend(f"!={upper_dev0.__replace__(dev=d)}" for d in range(run_length)) |
| return exclusions |
|
|
|
|
| def _encode_gaps(bounds: Sequence[Interval]) -> list[str] | None: |
| """Encode every between-interval gap as ``!=`` fragments, or ``None``. |
| |
| When each gap has an exclusion spelling, the intervals fuse into one |
| contiguous span (``==1.* | ==3.*`` is ``!=0.*,!=2.*,<4``): the outer |
| interval across all the bounds plus these exclusions. A gap with no |
| exclusion spelling makes the bounds a disjoint union, which no single |
| set expresses. |
| """ |
| exclusions: list[str] = [] |
|
|
| for index in range(1, len(bounds)): |
| gap = _encode_gap(bounds[index - 1][1], bounds[index][0]) |
| if gap is None: |
| return None |
| exclusions.extend(gap) |
|
|
| return exclusions |
|
|
|
|
| def _tighten_no_prereleases(bounds: tuple[Interval, ...]) -> tuple[Interval, ...]: |
| """Snap the range's final upper out of the pre-release band ``False`` drops. |
| |
| An exclusive upper at a final ``V`` admits the versions in ``[V.dev0, V)`` at |
| the bounds level, but a ``prereleases=False`` policy filters them all out, so |
| it accepts the same releases as ``<V`` (upper at ``V.dev0``). Snapping it lets |
| :meth:`VersionRange.to_specifier_set` reach the ``<V`` spelling. |
| |
| Only the last interval's upper is snapped, the one that gives a terser outer |
| bound. Inner uppers are left alone: snapping one turns its gap to the next |
| interval into a ``.dev0`` wildcard gap, which a far-apart neighbour would blow |
| up into an unbounded ``!=N.*`` chain. Those shapes recover as ``None`` here, |
| the same as under ``None`` / ``True``. The snap is conservative (it skips |
| boundary, pre-release, and local uppers); the caller keeps it only when it |
| stays release-equivalent, so an unsnapped shape falls back to the exact form. |
| """ |
| lower, upper = bounds[-1] |
| version = upper.version |
| if ( |
| isinstance(version, Version) |
| and not upper.inclusive |
| and not version.is_prerelease |
| and version.local is None |
| ): |
| upper = UpperBound(version.__replace__(dev=0), inclusive=False) |
| return (*bounds[:-1], (lower, upper)) |
| return bounds |
|
|
|
|
| class VersionRange: |
| """A set of :class:`~packaging.version.Version` values accepted by a |
| :class:`~packaging.specifiers.SpecifierSet`. |
| |
| Construct via :meth:`~packaging.specifiers.SpecifierSet.to_range`, or with |
| the :meth:`full`, :meth:`empty`, and :meth:`singleton` class methods. |
| Compose with :meth:`intersection`, :meth:`union`, :meth:`complement`, and |
| :meth:`difference` (or the ``&`` / ``|`` / ``~`` / ``-`` operators). Test |
| membership with ``in`` or :meth:`contains`, filter an iterable with |
| :meth:`filter`, and convert back to a |
| :class:`~packaging.specifiers.SpecifierSet` with :meth:`to_specifier_set`. |
| |
| The configured pre-release policy of the originating specifier set carries |
| onto the range and controls whether pre-releases are admitted under ``in``, |
| :meth:`contains`, and :meth:`filter`. With no configured policy, |
| :meth:`filter` also admits pre-releases in the autodetected opt-in region |
| (the versions a pre-release-naming specifier asked for). Set algebra keeps |
| that opt-in scoped to those versions, so unrelated pre-releases are not |
| admitted wholesale. |
| |
| :meth:`intersection`, :meth:`union`, :meth:`difference`, and the |
| :meth:`is_subset` / :meth:`is_superset` / :meth:`is_disjoint` predicates |
| require both operands to share the same configured policy. |
| |
| >>> r = SpecifierSet(">=1.0,<2.0").to_range() |
| >>> "1.5" in r |
| True |
| >>> "2.0" in r |
| False |
| >>> SpecifierSet(">=2.0,<1.0").to_range().is_empty |
| True |
| |
| PEP 440's ``===`` operator matches a candidate string verbatim |
| (case-insensitive) rather than a set of versions. Ranges built from |
| ``===`` specifiers still support membership, set operations, and conversion |
| back to a :class:`~packaging.specifiers.SpecifierSet`; matching follows the |
| literal-equality rule. A ``===`` literal that names a pre-release is |
| admitted under the default policy by both :meth:`contains` and |
| :meth:`filter`, since it was named outright. |
| |
| .. versionadded:: 26.3 |
| """ |
|
|
| __slots__ = ( |
| "_admit", |
| "_admit_arbitrary", |
| "_bounds", |
| "_pre_region", |
| "_prereleases_configured", |
| "_reject", |
| ) |
|
|
| |
| _bounds: tuple[Interval, ...] |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _admit_arbitrary: bool |
|
|
| |
| |
| _admit: frozenset[str] |
|
|
| |
| |
| |
| _reject: frozenset[str] |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _pre_region: tuple[Interval, ...] |
|
|
| |
| |
| |
| |
| |
| _prereleases_configured: bool | None |
|
|
| def __new__(cls, *args: object, **kwargs: object) -> VersionRange: |
| raise TypeError( |
| "cannot create 'VersionRange' instances directly; use " |
| "SpecifierSet.to_range(), VersionRange.full(), " |
| "VersionRange.empty(), or VersionRange.singleton() instead" |
| ) |
|
|
| @classmethod |
| def _build( |
| cls, |
| bounds: tuple[Interval, ...], |
| admit: frozenset[str] = frozenset(), |
| reject: frozenset[str] = frozenset(), |
| admit_arbitrary: bool = False, |
| *, |
| pre_region: tuple[Interval, ...] = (), |
| prereleases_configured: bool | None = None, |
| ) -> VersionRange: |
| """Internal factory; bypasses :meth:`__new__`. |
| |
| Canonicalizes the bounds so equal version sets share one representation, |
| then drops admit literals the bounds already admit and reject literals |
| the bounds do not match anyway. Reject wins over admit on overlap. The |
| pre-release policy is set here and never reassigned afterwards; |
| ``pre_region`` is canonicalized like the bounds and clipped to them, |
| or dropped when a configured policy makes it inert. |
| """ |
| bounds = _canonicalize(bounds) |
|
|
| if admit and reject: |
| admit = admit - reject |
| if admit: |
| admit = frozenset( |
| literal |
| for literal in admit |
| if not _struct_admits(bounds, admit_arbitrary, literal) |
| ) |
| if reject: |
| reject = frozenset( |
| literal |
| for literal in reject |
| if _struct_admits(bounds, admit_arbitrary, literal) |
| ) |
|
|
| instance = object.__new__(cls) |
| instance._bounds = bounds |
| instance._admit = admit |
| instance._reject = reject |
| instance._admit_arbitrary = admit_arbitrary |
| instance._prereleases_configured = prereleases_configured |
|
|
| |
| |
| |
| |
| if prereleases_configured is not None or not pre_region: |
| instance._pre_region = () |
| else: |
| instance._pre_region = tuple( |
| intersect_ranges(_canonicalize(pre_region), bounds) |
| ) |
|
|
| return instance |
|
|
| def _has_literals(self) -> bool: |
| return bool(self._admit) or bool(self._reject) |
|
|
| def _arbitrary_active(self) -> bool: |
| """True when ``_admit_arbitrary`` actually admits non-version strings. |
| |
| The flag rides through set algebra but only fires admission on full |
| bounds. Intersection and difference drop it when the bounds shrink, so |
| away from full bounds it survives only on empty-bounds ranges, where |
| it keeps ``~~full() == full()`` and union idempotent. |
| """ |
| return self._admit_arbitrary and self._bounds == FULL_RANGE |
|
|
| def _is_plain(self) -> bool: |
| """True when membership is decided by ``_bounds`` alone, enabling the |
| bounds-only fast paths in :meth:`is_subset` and :meth:`is_disjoint`. |
| """ |
| return ( |
| not self._has_literals() |
| and not self._admit_arbitrary |
| and self._prereleases_configured is not False |
| ) |
|
|
| def _check_policy_compat(self, other: VersionRange) -> None: |
| """Refuse combining ranges with different pre-release policies.""" |
| if not isinstance(other, VersionRange): |
| raise TypeError(f"expected VersionRange, got {type(other).__name__}") |
| if self._prereleases_configured != other._prereleases_configured: |
| raise ValueError( |
| "Cannot combine VersionRange operands with different " |
| f"pre-release policies: {self._prereleases_configured!r} " |
| f"and {other._prereleases_configured!r}" |
| ) |
|
|
| def _merged_region(self, other: VersionRange) -> tuple[Interval, ...]: |
| """Union of ``self`` and ``other``'s opt-in regions. |
| |
| Used by :meth:`union` and :meth:`intersection`; :meth:`_build` clips the |
| merge to the result bounds. A configured operand carries an empty region, |
| so it contributes nothing to the merge. |
| """ |
| |
| |
| if not other._pre_region: |
| return self._pre_region |
| if not self._pre_region: |
| return other._pre_region |
|
|
| |
| |
| return tuple(_union_ranges(self._pre_region, other._pre_region)) |
|
|
| def _with_policy( |
| self, *, pre_region: tuple[Interval, ...], configured: bool | None |
| ) -> VersionRange: |
| """A structural copy of this range carrying the given pre-release policy.""" |
| return self._build( |
| self._bounds, |
| admit=self._admit, |
| reject=self._reject, |
| admit_arbitrary=self._admit_arbitrary, |
| pre_region=pre_region, |
| prereleases_configured=configured, |
| ) |
|
|
| @classmethod |
| def empty(cls, *, prereleases: bool | None = None) -> VersionRange: |
| """Return the empty range. No version satisfies it. |
| |
| >>> VersionRange.empty().is_empty |
| True |
| >>> "1.0" in VersionRange.empty() |
| False |
| """ |
| return cls._build((), prereleases_configured=prereleases) |
|
|
| @classmethod |
| def full( |
| cls, *, admit_arbitrary: bool = True, prereleases: bool | None = None |
| ) -> VersionRange: |
| """Return the full range. Every PEP 440 version satisfies it. |
| |
| ``admit_arbitrary=False`` restricts the range to PEP 440 versions only |
| (matching the same versions as ``SpecifierSet(">=0.dev0").to_range()``); |
| its complement is :meth:`empty`. The flag propagates through set algebra |
| and is part of equality. Default ``True`` so that ``r & full()`` |
| preserves ``r``'s own flag structurally. |
| |
| >>> "1.0" in VersionRange.full() |
| True |
| >>> "wat" in VersionRange.full() |
| True |
| >>> "wat" in VersionRange.full(admit_arbitrary=False) |
| False |
| """ |
| return cls._build( |
| FULL_RANGE, |
| admit_arbitrary=admit_arbitrary, |
| prereleases_configured=prereleases, |
| ) |
|
|
| @classmethod |
| def singleton( |
| cls, version: Version | str, *, prereleases: bool | None = None |
| ) -> VersionRange: |
| """Return the strict singleton range ``{version}``. |
| |
| Built as the closed interval ``[version, version]`` with strict |
| equality. ``Specifier("==V")`` matches ``V+local`` too, so the strict |
| singleton is narrower: |
| |
| >>> "1.0+local" in VersionRange.singleton("1.0") |
| False |
| >>> "1.0+local" in SpecifierSet("==1.0").to_range() |
| True |
| |
| :raises packaging.version.InvalidVersion: if version is a string that |
| does not parse as a PEP 440 version. |
| """ |
| if not isinstance(version, Version): |
| version = Version(version) |
|
|
| lower = LowerBound(version, True) |
| upper = UpperBound(version, True) |
|
|
| |
| |
| return cls._build( |
| _canonical_floor(((lower, upper),)), |
| prereleases_configured=prereleases, |
| ) |
|
|
| def intersection(self, other: VersionRange) -> VersionRange: |
| """Range containing exactly the versions in both self and other. |
| |
| Both operands must share the same configured pre-release policy; |
| otherwise :exc:`ValueError` is raised. |
| |
| >>> a = SpecifierSet(">=1.0").to_range() |
| >>> b = SpecifierSet("<2.0").to_range() |
| >>> a.intersection(b) == SpecifierSet(">=1.0,<2.0").to_range() |
| True |
| """ |
| self._check_policy_compat(other) |
|
|
| configured = self._prereleases_configured |
| new_bounds = tuple(intersect_ranges(self._bounds, other._bounds)) |
| new_region = self._merged_region(other) |
|
|
| |
| |
| |
| combined_arb = ( |
| self._admit_arbitrary and other._admit_arbitrary and bool(new_bounds) |
| ) |
|
|
| if not self._has_literals() and not other._has_literals(): |
| return self._build( |
| new_bounds, |
| admit_arbitrary=combined_arb, |
| pre_region=new_region, |
| prereleases_configured=configured, |
| ) |
|
|
| return self._combine_literals( |
| other, |
| new_bounds, |
| op=_SetOp.INTERSECTION, |
| admit_arbitrary=combined_arb, |
| pre_region=new_region, |
| prereleases_configured=configured, |
| ) |
|
|
| def union(self, other: VersionRange) -> VersionRange: |
| """Range containing every version in self or other. |
| |
| Both operands must share the same configured pre-release policy; |
| otherwise :exc:`ValueError` is raised. |
| |
| >>> a = VersionRange.singleton("1.0") |
| >>> b = VersionRange.singleton("2.0") |
| >>> "1.0" in a.union(b) and "2.0" in a.union(b) |
| True |
| >>> "1.5" in a.union(b) |
| False |
| """ |
| self._check_policy_compat(other) |
|
|
| configured = self._prereleases_configured |
| new_bounds = tuple(_union_ranges(self._bounds, other._bounds)) |
| new_region = self._merged_region(other) |
|
|
| |
| |
| |
| if new_bounds: |
| combined_arb = (self._admit_arbitrary and bool(self._bounds)) or ( |
| other._admit_arbitrary and bool(other._bounds) |
| ) |
| else: |
| |
| combined_arb = self._admit_arbitrary or other._admit_arbitrary |
|
|
| if not self._has_literals() and not other._has_literals(): |
| return self._build( |
| new_bounds, |
| admit_arbitrary=combined_arb, |
| pre_region=new_region, |
| prereleases_configured=configured, |
| ) |
|
|
| return self._combine_literals( |
| other, |
| new_bounds, |
| op=_SetOp.UNION, |
| admit_arbitrary=combined_arb, |
| pre_region=new_region, |
| prereleases_configured=configured, |
| ) |
|
|
| def complement(self) -> VersionRange: |
| """Range containing every version not in self. |
| |
| Preserves the configured pre-release policy. On the version set, double |
| negation holds for a range with no ``===`` literals (the arbitrary-string |
| flag round-trips, so ``~~full() == full()``); for ``===`` ranges |
| complement is one-way. The opt-in region is not restored (see below). |
| |
| The opt-in region is dropped: a complement is an exclusion, and an |
| exclusion expresses no pre-release preference. This is what lets |
| ``a & ~b`` shed ``b``'s opt-in, so an excluded ``b`` never force-admits a |
| pre-release into the result. Complement stays involutive on the version |
| set, but not on the opt-in region: ``~~r`` covers the same versions as |
| ``r`` yet force-admits none of its pre-releases. |
| |
| >>> r = SpecifierSet(">=1.0").to_range() |
| >>> "0.5" in r.complement() |
| True |
| >>> "1.5" in r.complement() |
| False |
| >>> r.complement().complement() == r |
| True |
| """ |
| |
| |
| return self._build( |
| tuple(_complement_ranges(self._bounds)), |
| admit=self._reject, |
| reject=self._admit, |
| admit_arbitrary=self._admit_arbitrary, |
| pre_region=(), |
| prereleases_configured=self._prereleases_configured, |
| ) |
|
|
| def difference(self, other: VersionRange) -> VersionRange: |
| """Range containing the versions in self but not in other. |
| |
| Matches ``self & ~other`` on the version set and the opt-in region; |
| ``other`` acts as a bounds-only exclusion that grants no opt-in. The |
| arbitrary-string flag survives only when ``other`` removed no versions: |
| a difference that shrinks the bounds forgets it, as ``self & ~other`` |
| would, so no later widening union can revive it. They still part on |
| ``===`` literals, whose complement is one-way: a ``===`` literal stays |
| when ``self`` admits it and ``other`` does not. Both operands must |
| share the same configured pre-release policy (as :meth:`intersection` |
| and :meth:`union` require); otherwise :exc:`ValueError` is raised. |
| ``a - empty()`` returns a range equal to ``a``. |
| |
| >>> a = SpecifierSet(">=1.0").to_range() |
| >>> b = SpecifierSet(">=2.0").to_range() |
| >>> "1.5" in a.difference(b) |
| True |
| >>> "2.0" in a.difference(b) |
| False |
| >>> a.difference(VersionRange.empty()) == a |
| True |
| """ |
| self._check_policy_compat(other) |
|
|
| |
| if not other._bounds and not other._admit: |
| return self |
|
|
| |
| |
| new_bounds = tuple( |
| intersect_ranges(self._bounds, _complement_ranges(other._bounds)) |
| ) |
|
|
| |
| |
| |
| new_region: tuple[Interval, ...] = () |
| if self._prereleases_configured is None: |
| new_region = self._pre_region |
|
|
| |
| |
| |
| |
| combined_arb = self._admit_arbitrary and new_bounds == self._bounds |
|
|
| if not self._has_literals() and not other._has_literals(): |
| return self._build( |
| new_bounds, |
| admit_arbitrary=combined_arb, |
| pre_region=new_region, |
| prereleases_configured=self._prereleases_configured, |
| ) |
|
|
| return self._combine_literals( |
| other, |
| new_bounds, |
| op=_SetOp.DIFFERENCE, |
| admit_arbitrary=combined_arb, |
| pre_region=new_region, |
| prereleases_configured=self._prereleases_configured, |
| ) |
|
|
| def _combine_literals( |
| self, |
| other: VersionRange, |
| new_bounds: tuple[Interval, ...], |
| *, |
| op: _SetOp, |
| admit_arbitrary: bool, |
| pre_region: tuple[Interval, ...], |
| prereleases_configured: bool | None, |
| ) -> VersionRange: |
| """Resolve admit/reject for ``self`` ``op`` ``other`` over their literals.""" |
| admits: set[str] = set() |
| rejects: set[str] = set() |
|
|
| |
| for literal in self._admit | self._reject | other._admit | other._reject: |
| self_in = self._matches_literal(literal) |
| other_in = other._matches_literal(literal) |
|
|
| if op is _SetOp.INTERSECTION: |
| want = self_in and other_in |
| elif op is _SetOp.UNION: |
| want = self_in or other_in |
| else: |
| want = self_in and not other_in |
|
|
| if want: |
| admits.add(literal) |
| else: |
| rejects.add(literal) |
|
|
| return self._build( |
| new_bounds, |
| admit=frozenset(admits), |
| reject=frozenset(rejects), |
| admit_arbitrary=admit_arbitrary, |
| pre_region=pre_region, |
| prereleases_configured=prereleases_configured, |
| ) |
|
|
| def _matches_literal(self, literal: str) -> bool: |
| """Whether literal (case-folded) matches this range's predicate.""" |
| if literal in self._reject: |
| return False |
| if literal in self._admit: |
| return True |
|
|
| parsed = coerce_version(literal) |
| if parsed is None: |
| return self._arbitrary_active() |
| return matches_bounds_only(self._bounds, parsed) |
|
|
| def __and__(self, other: object) -> VersionRange: |
| """Operator alias for :meth:`intersection`.""" |
| if not isinstance(other, VersionRange): |
| return NotImplemented |
| return self.intersection(other) |
|
|
| def __or__(self, other: object) -> VersionRange: |
| """Operator alias for :meth:`union`.""" |
| if not isinstance(other, VersionRange): |
| return NotImplemented |
| return self.union(other) |
|
|
| def __invert__(self) -> VersionRange: |
| """Operator alias for :meth:`complement`.""" |
| return self.complement() |
|
|
| def __sub__(self, other: object) -> VersionRange: |
| """Operator alias for :meth:`difference`.""" |
| if not isinstance(other, VersionRange): |
| return NotImplemented |
| return self.difference(other) |
|
|
| def is_subset(self, other: VersionRange) -> bool: |
| """Return whether every member of self is also a member of other. |
| |
| On versions and ``===`` literals this is |
| ``self.difference(other).is_empty``: subtracting other leaves nothing |
| behind. A live arbitrary admission (the flag at full bounds) is only a |
| subset of another live one. |
| |
| Both operands must share the same configured pre-release policy; |
| otherwise :exc:`ValueError` is raised. |
| |
| >>> inner = SpecifierSet(">=1.5,<1.8").to_range() |
| >>> outer = SpecifierSet(">=1.0,<2.0").to_range() |
| >>> inner.is_subset(outer) |
| True |
| >>> outer.is_subset(inner) |
| False |
| >>> VersionRange.empty().is_subset(outer) |
| True |
| """ |
| self._check_policy_compat(other) |
|
|
| |
| |
| if self._arbitrary_active() and not other._arbitrary_active(): |
| return False |
|
|
| |
| if self._is_plain() and other._is_plain(): |
| return not intersect_ranges(self._bounds, _complement_ranges(other._bounds)) |
|
|
| |
| |
| return self.difference(other).is_empty |
|
|
| def is_superset(self, other: VersionRange) -> bool: |
| """Return whether every member of other is also a member of self. |
| |
| The mirror of :meth:`is_subset`: ``a.is_superset(b)`` is |
| ``b.is_subset(a)``. |
| |
| Both operands must share the same configured pre-release policy; |
| otherwise :exc:`ValueError` is raised. |
| |
| >>> outer = SpecifierSet(">=1.0,<2.0").to_range() |
| >>> outer.is_superset(SpecifierSet(">=1.5,<1.8").to_range()) |
| True |
| """ |
| |
| self._check_policy_compat(other) |
| return other.is_subset(self) |
|
|
| def is_disjoint(self, other: VersionRange) -> bool: |
| """Return whether self and other share no member. |
| |
| Equivalent to ``(self & other).is_empty``. |
| |
| Both operands must share the same configured pre-release policy; |
| otherwise :exc:`ValueError` is raised. |
| |
| >>> a = SpecifierSet(">=1.0,<2.0").to_range() |
| >>> a.is_disjoint(SpecifierSet(">=2.0,<3.0").to_range()) |
| True |
| >>> a.is_disjoint(SpecifierSet(">=1.5,<2.5").to_range()) |
| False |
| """ |
| self._check_policy_compat(other) |
|
|
| |
| if self._is_plain() and other._is_plain(): |
| return not intersect_ranges(self._bounds, other._bounds) |
| return self.intersection(other).is_empty |
|
|
| def _same_releases(self, other: VersionRange) -> bool: |
| """Whether self and other admit the same non-pre-release versions. |
| |
| Used by :meth:`to_specifier_set` under a ``prereleases=False`` policy, |
| where pre-releases are unobservable: the symmetric difference is empty |
| exactly when the two ranges accept the same releases. Both operands |
| carry that policy, so the difference below reads emptiness through it. |
| """ |
| return self.difference(other).is_empty and other.difference(self).is_empty |
|
|
| @typing.overload |
| def filter( |
| self, |
| iterable: Iterable[UnparsedVersionVar], |
| prereleases: bool | None = None, |
| key: None = ..., |
| ) -> Iterator[UnparsedVersionVar]: ... |
|
|
| @typing.overload |
| def filter( |
| self, |
| iterable: Iterable[T], |
| prereleases: bool | None = None, |
| key: Callable[[T], UnparsedVersion] = ..., |
| ) -> Iterator[T]: ... |
|
|
| def filter( |
| self, |
| iterable: Iterable[Any], |
| prereleases: bool | None = None, |
| key: Callable[[Any], Version | str] | None = None, |
| ) -> Iterator[Any]: |
| """Yield items from iterable whose version falls inside the range. |
| |
| With prereleases ``None`` the PEP 440 default applies: pre-releases are |
| buffered and only emitted if no final release in iterable is in range, |
| except that a pre-release inside the autodetected opt-in region, or named |
| outright by a ``===`` literal, is force-admitted in place (as |
| ``prereleases=True`` would yield it). A flushed buffer comes after |
| every in-place yield, so the output is not version-sorted. |
| |
| The signature mirrors |
| :meth:`~packaging.specifiers.SpecifierSet.filter`. |
| |
| >>> r = SpecifierSet(">=1.0,<2.0").to_range() |
| >>> list(r.filter(["0.9", "1.5", "2.0"])) |
| ['1.5'] |
| """ |
| region: tuple[Interval, ...] = () |
| if prereleases is None: |
| |
| |
| prereleases = self._prereleases_configured |
| region = self._pre_region |
|
|
| arbitrary_active = self._arbitrary_active() |
| if not self._admit and not self._reject and not arbitrary_active: |
| |
| |
| |
| |
| if region and region == self._bounds: |
| return filter_by_ranges(self._bounds, iterable, key, True) |
| return filter_by_ranges(self._bounds, iterable, key, prereleases, region) |
| return self._filter_with_admission( |
| iterable, key, prereleases, arbitrary_active, region |
| ) |
|
|
| def _filter_with_admission( |
| self, |
| iterable: Iterable[Any], |
| key: Callable[[Any], Version | str] | None, |
| prereleases: bool | None, |
| arbitrary_active: bool, |
| region: tuple[Interval, ...], |
| ) -> Iterator[Any]: |
| """Filter for ranges with admit/reject literals or live arbitrary |
| admission (including the universal ``SpecifierSet("")`` range).""" |
| admit_set = self._admit |
| reject_set = self._reject |
|
|
| def admit(item: Any) -> tuple[bool, Version | None, bool]: |
| raw: Version | str = item if key is None else key(item) |
| raw_lower = str(raw).lower() |
|
|
| if reject_set and raw_lower in reject_set: |
| return False, None, False |
| if admit_set and raw_lower in admit_set: |
| |
| return True, coerce_version(raw), True |
|
|
| parsed = coerce_version(raw) |
| if parsed is None: |
| return arbitrary_active, None, False |
| if not matches_bounds_only(self._bounds, parsed): |
| return False, None, False |
| return True, parsed, False |
|
|
| if prereleases is True: |
| for item in iterable: |
| ok, _, _ = admit(item) |
| if ok: |
| yield item |
| return |
|
|
| if prereleases is False: |
| for item in iterable: |
| ok, parsed, _ = admit(item) |
| if not ok: |
| continue |
| if parsed is not None and parsed.is_prerelease: |
| continue |
| yield item |
| return |
|
|
| |
| |
| all_nonfinal: list[Any] = [] |
| arbitrary_strings: list[Any] = [] |
| found_final = False |
|
|
| for item in iterable: |
| ok, parsed, by_literal = admit(item) |
| if not ok: |
| continue |
|
|
| if parsed is None: |
| if found_final: |
| yield item |
| else: |
| arbitrary_strings.append(item) |
| all_nonfinal.append(item) |
| continue |
|
|
| if not parsed.is_prerelease: |
| if not found_final: |
| yield from arbitrary_strings |
| arbitrary_strings.clear() |
| found_final = True |
| yield item |
| continue |
|
|
| |
| |
| |
| if by_literal or (region and matches_bounds_only(region, parsed)): |
| yield item |
| continue |
|
|
| if not found_final: |
| all_nonfinal.append(item) |
|
|
| if not found_final: |
| yield from all_nonfinal |
|
|
| @classmethod |
| def _from_specifier_set(cls, specifier_set: SpecifierSet) -> VersionRange: |
| """Build the range accepted by ``specifier_set``. |
| |
| Friend constructor for :meth:`~packaging.specifiers.SpecifierSet.to_range`. |
| The intersection of every specifier in the set: an empty set yields the |
| full range, an unsatisfiable set yields the empty range, and ``===`` |
| specifiers contribute literal-string admission. |
| """ |
| if not specifier_set: |
| result = cls.full() |
| elif not specifier_set._has_arbitrary: |
| result = cls._build( |
| bounds=_canonical_floor(tuple(specifier_set._get_ranges())) |
| ) |
| else: |
| result = cls.full() |
| for spec in specifier_set: |
| if spec.operator == "===": |
| operand = cls._build( |
| bounds=(), admit=frozenset({spec.version.lower()}) |
| ) |
| else: |
| operand = cls._build( |
| bounds=_canonical_floor(tuple(spec._to_ranges())) |
| ) |
| result = result.intersection(operand) |
|
|
| |
| |
| |
| |
| region: list[Interval] = [] |
| if specifier_set._prereleases is None: |
| for spec in specifier_set: |
| |
| if spec.operator != "===" and spec.prereleases: |
| spec_bounds = _canonical_floor(tuple(spec._to_ranges())) |
| region = _union_ranges(region, spec_bounds) |
|
|
| return result._with_policy( |
| pre_region=tuple(region), |
| configured=specifier_set._prereleases, |
| ) |
|
|
| def to_specifier_set(self) -> SpecifierSet | None: |
| """Return a :class:`~packaging.specifiers.SpecifierSet` matching the same |
| versions as self, or ``None`` if no single set expresses it. |
| |
| PEP 440 has no syntax for the strict singleton ``{V}`` (an exclusive |
| plain-version bound), a disjoint union of two or more intervals, or a |
| partial pre-release opt-in region, so ranges built by set algebra often |
| return ``None``. A gap that takes more than ``_MAX_EXCLUSION_RUN`` |
| contiguous ``!=`` exclusions to spell returns ``None`` too, |
| rather than a pathologically long chain; reaching that cap takes either |
| set algebra or a specifier set that already spells the gap out with |
| over a hundred contiguous ``!=N.*`` exclusions. An empty range maps to |
| ``SpecifierSet("<0")``, unless it still carries the arbitrary-string |
| flag (which no set reproduces), and a full range that admits arbitrary |
| strings maps to ``SpecifierSet("")``. |
| |
| A range built from a :class:`~packaging.specifiers.SpecifierSet` |
| re-encodes, short of that exclusion cap. The result is the simplest |
| candidate whose own |
| :meth:`~packaging.specifiers.SpecifierSet.to_range` reproduces self |
| exactly (bounds, ``===`` literals, and the opt-in region are all part of |
| equality), so it filters the same versions. Two cases relax that |
| exactness without changing what is filtered: an empty range recovers as |
| the canonical empty range (same versions, none, but not self's bounds), |
| and under a ``prereleases=False`` policy the result need only match self's |
| releases, so ``(-inf, 3.14)`` recovers as the tighter ``<3.14`` rather |
| than ``!=3.14,<=3.14``. |
| |
| Each call encodes a handful of candidate spellings and keeps the |
| simplest one that verifies, where verifying means parsing the candidate |
| and round-tripping it through |
| :meth:`~packaging.specifiers.SpecifierSet.to_range`. The work grows |
| with the number of intervals and exclusions in the range, and the |
| result is not cached, so convert once and reuse the returned set rather |
| than converting per candidate version in a hot loop. |
| |
| >>> str(SpecifierSet(">=1.0,<2.0").to_range().to_specifier_set()) |
| '<2.0,>=1.0' |
| >>> str(SpecifierSet("==1.0").to_range().to_specifier_set()) |
| '==1.0' |
| >>> VersionRange.singleton("1.5").to_specifier_set() is None |
| True |
| """ |
| from .specifiers import InvalidSpecifier, SpecifierSet |
|
|
| configured = self._prereleases_configured |
|
|
| if self._reject: |
| return None |
| if self._admit_arbitrary and self._bounds != FULL_RANGE: |
| return None |
| if self.is_empty: |
| |
| |
| |
| return SpecifierSet("<0", prereleases=configured) |
|
|
| if not self._bounds: |
| |
| if len(self._admit) != 1: |
| return None |
| (literal,) = self._admit |
| bases = [f"==={literal}"] |
| elif self._admit: |
| |
| return None |
| elif self._bounds == FULL_RANGE: |
| bases = ["" if self._admit_arbitrary else ">=0.dev0"] |
| else: |
| |
| |
| |
| |
| layouts = [self._bounds] |
| if configured is False: |
| tightened = _tighten_no_prereleases(self._bounds) |
| if tightened != self._bounds: |
| layouts.append(tightened) |
|
|
| |
| |
| |
| |
| bases = [] |
| for layout in layouts: |
| exclusions = _encode_gaps(layout) |
| if exclusions is None: |
| continue |
|
|
| for keep_dev0 in (False, True): |
| outer = _encode_interval(layout[0][0], layout[-1][1], keep_dev0) |
| if outer is None: |
| continue |
| base = ",".join(outer + exclusions) |
| if base not in bases: |
| bases.append(base) |
|
|
| |
| |
| |
| |
| add_floor = configured is None and self._pre_region == self._bounds |
|
|
| |
| |
| |
| |
| |
| |
| best: SpecifierSet | None = None |
| best_key = (0, 0) |
|
|
| for base in bases: |
| candidates = [base] |
| if add_floor: |
| candidates.append(f"{base},>=0.dev0" if base else ">=0.dev0") |
|
|
| for spec_str in candidates: |
| |
| |
| |
| |
| |
| try: |
| recovered = SpecifierSet(spec_str, prereleases=configured) |
| except InvalidSpecifier: |
| continue |
| |
| |
| |
| key = (len(recovered), len(str(recovered))) |
| if best is not None and key >= best_key: |
| continue |
|
|
| |
| |
| candidate = recovered.to_range() |
| matches = candidate == self or ( |
| configured is False and self._same_releases(candidate) |
| ) |
| if matches: |
| best, best_key = recovered, key |
|
|
| return best |
|
|
| @property |
| def is_empty(self) -> bool: |
| """``True`` if no version or string satisfies this range. |
| |
| Agrees with :meth:`~packaging.specifiers.SpecifierSet.is_unsatisfiable`, |
| including the pre-release policy: a range whose only members are |
| pre-releases is empty when that policy excludes them. |
| |
| >>> SpecifierSet(">=2,<1").to_range().is_empty |
| True |
| >>> SpecifierSet(">=1,<2").to_range().is_empty |
| False |
| >>> SpecifierSet("==1.0a1", prereleases=False).to_range().is_empty |
| True |
| """ |
| |
| |
| if self._arbitrary_active(): |
| return False |
|
|
| excludes_prereleases = self._prereleases_configured is False |
| for literal in self._admit: |
| if excludes_prereleases: |
| parsed = coerce_version(literal) |
| if parsed is not None and parsed.is_prerelease: |
| continue |
| return False |
|
|
| if not self._bounds: |
| return True |
|
|
| return excludes_prereleases and ranges_are_prerelease_only(self._bounds) |
|
|
| def contains( |
| self, |
| item: Version | str, |
| prereleases: bool | None = None, |
| installed: bool | None = None, |
| ) -> bool: |
| """Return whether item is contained in this range. |
| |
| :param item: a version string or :class:`~packaging.version.Version`. |
| :param prereleases: whether to match pre-releases. ``None`` (default) |
| uses the range's own policy. |
| :param installed: when ``True``, accept a pre-release item even if the |
| range would not otherwise allow it. |
| |
| Unlike :meth:`filter`, this does not consult the autodetected pre-release |
| opt-in region; it reads only the configured policy. This mirrors |
| :meth:`~packaging.specifiers.SpecifierSet.contains` versus |
| :meth:`~packaging.specifiers.SpecifierSet.filter`. |
| |
| Unparsable strings do not match, except where the full |
| ``SpecifierSet`` would also match: the full range admits any string, |
| and a ``===`` range admits items equal to the literal |
| case-insensitively. |
| |
| >>> r = SpecifierSet(">=1.0,<2.0").to_range() |
| >>> r.contains("1.5") |
| True |
| >>> r.contains("2.0") |
| False |
| |
| :raises TypeError: if item is not a str or Version. |
| """ |
| if not isinstance(item, (str, Version)): |
| raise TypeError( |
| f"VersionRange.contains() expected str or Version, " |
| f"got {type(item).__name__}" |
| ) |
|
|
| parsed: Version | None = item if isinstance(item, Version) else None |
| if installed and parsed is None: |
| parsed = coerce_version(item) |
| if installed and parsed is not None and parsed.is_prerelease: |
| prereleases = True |
|
|
| effective_pre = ( |
| self._prereleases_configured if prereleases is None else prereleases |
| ) |
|
|
| if self._admit or self._reject: |
| item_str = str(item).lower() |
| if item_str in self._reject: |
| return False |
| if item_str in self._admit: |
| if effective_pre is False: |
| literal_parsed = coerce_version(item_str) |
| if literal_parsed is not None and literal_parsed.is_prerelease: |
| return False |
| return True |
|
|
| if not isinstance(item, Version): |
| if parsed is None: |
| parsed = coerce_version(item) |
| if parsed is None: |
| return self._arbitrary_active() |
| item = parsed |
|
|
| if effective_pre is False and item.is_prerelease: |
| return False |
| return matches_bounds_only(self._bounds, item) |
|
|
| def __contains__(self, item: Version | str) -> bool: |
| """Return whether item is contained in this range. |
| |
| Forwards to :meth:`contains` with default arguments. |
| |
| >>> "1.5" in SpecifierSet(">=1.0,<2.0").to_range() |
| True |
| """ |
| return self.contains(item) |
|
|
| def __eq__(self, other: object) -> bool: |
| """Structural equality. |
| |
| Compares the bounds, the ``===`` admit/reject literals, the |
| arbitrary-string flag, the configured pre-release policy, and the |
| opt-in region, not just the version set. Keying on the region makes |
| equality a congruence (equal ranges stay equal under further operations), |
| so equal implies same :meth:`contains` and :meth:`filter`, but not the |
| converse: an empty range keeps the flag it was built with, so two empty |
| ranges need not be equal. |
| |
| Different specifiers for the same range fold to one canonical form: |
| |
| >>> SpecifierSet(">1.0a1").to_range() == SpecifierSet(">=1.0a2.dev0").to_range() |
| True |
| |
| The opt-in region is part of equality, so ``<=1.0`` (no pre-releases) and |
| ``<1.0.post0.dev0`` (autodetects a ``.dev`` opt-in) cover the same |
| versions yet compare unequal: |
| |
| >>> le, lt = SpecifierSet("<=1.0"), SpecifierSet("<1.0.post0.dev0") |
| >>> le.to_range() == lt.to_range() |
| False |
| |
| >>> r = SpecifierSet(">=1.0,<2.0").to_range() |
| >>> r == SpecifierSet(">=1.0,<2.0").to_range() |
| True |
| """ |
| if not isinstance(other, VersionRange): |
| return NotImplemented |
| return ( |
| self._bounds == other._bounds |
| and self._admit == other._admit |
| and self._reject == other._reject |
| and self._admit_arbitrary == other._admit_arbitrary |
| and self._prereleases_configured == other._prereleases_configured |
| and self._pre_region == other._pre_region |
| ) |
|
|
| def __hash__(self) -> int: |
| return hash( |
| ( |
| self._bounds, |
| self._admit, |
| self._reject, |
| self._admit_arbitrary, |
| self._prereleases_configured, |
| self._pre_region, |
| ) |
| ) |
|
|
| def __repr__(self) -> str: |
| """Human-readable representation for debugging. |
| |
| >>> SpecifierSet(">=1.0,<2.0").to_range() |
| <VersionRange '[1.0, 2.0.dev0)'> |
| >>> SpecifierSet("").to_range() |
| <VersionRange '(-inf, +inf)' arbitrary> |
| >>> SpecifierSet(">=2.0,<1.0").to_range() |
| <VersionRange '(empty)'> |
| """ |
| |
| parts: list[str] = [] |
| if self._bounds: |
| parts.append(_format_intervals(self._bounds)) |
| if self._admit: |
| parts.append("{" + ", ".join(sorted(self._admit)) + "}") |
| body = " | ".join(parts) if parts else "(empty)" |
|
|
| |
| if self._reject: |
| body = f"{body} \\ {{{', '.join(sorted(self._reject))}}}" |
|
|
| |
| tail = "" |
| if self._admit_arbitrary: |
| tail += " arbitrary" |
| if self._prereleases_configured is not None: |
| tail += f" pre={self._prereleases_configured}" |
| if self._pre_region: |
| tail += f" pre-region={_format_intervals(self._pre_region)!r}" |
|
|
| return f"<{self.__class__.__name__} {body!r}{tail}>" |
|
|