Source code for spacecore._checks

from __future__ import annotations

import inspect
from functools import wraps
from typing import Any, Callable, Literal, TypeVar, cast


CheckLevel = Literal["none", "cheap", "standard", "strict"]

CHECK_LEVELS: tuple[CheckLevel, ...] = ("none", "cheap", "standard", "strict")
_CHECK_LEVEL_ORDER = {level: index for index, level in enumerate(CHECK_LEVELS)}


def require_mutually_exclusive(
    name_a: str,
    value_a: object,
    name_b: str,
    value_b: object,
    *,
    verb: str = "Use",
) -> None:
    """Raise ``TypeError`` when two mutually exclusive arguments are both supplied.

    Parameters
    ----------
    name_a, name_b : str
        Public names of the two arguments, used in the error message.
    value_a, value_b : object
        Supplied values. Either being ``None`` means "not provided".
    verb : str, optional
        Leading verb of the error message (``"Use"`` or ``"Specify"``).

    Raises
    ------
    TypeError
        If both ``value_a`` and ``value_b`` are not ``None``.
    """
    if value_a is not None and value_b is not None:
        raise TypeError(f"{verb} either {name_a} or {name_b}, not both.")


def normalize_check_level(
    check_level: CheckLevel | str | None = None,
    *,
    default: CheckLevel = "standard",
) -> CheckLevel:
    """Return ``check_level`` (or ``default`` when ``None``), validated against the known levels."""
    level = default if check_level is None else check_level
    if level not in _CHECK_LEVEL_ORDER:
        allowed = ", ".join(repr(item) for item in CHECK_LEVELS)
        raise ValueError(f"Unknown check_level {level!r}. Expected one of: {allowed}.")
    return level  # type: ignore[return-value]


def check_level_at_least(actual: CheckLevel, required: CheckLevel) -> bool:
    """Return whether ``actual`` includes checks assigned to ``required``."""
    return _CHECK_LEVEL_ORDER[actual] >= _CHECK_LEVEL_ORDER[required]


def minimum_check_level(levels: tuple[CheckLevel, ...]) -> CheckLevel:
    """Return the least expensive policy shared by all supplied levels."""
    return min(levels, key=_CHECK_LEVEL_ORDER.__getitem__, default="none")


# Cap on every object's check level while a backend traces (jit, vmap, compile).
# Value checks cannot run on a tracer, so the default turns checks off under tracing.
_traced_check_level: CheckLevel = "none"


[docs] def set_traced_check_level(level: CheckLevel | None) -> None: """ Set the process-wide cap on check levels while a backend is tracing. Inside ``jax.jit``, ``jax.vmap`` or ``torch.compile`` an object checks at the lower of its own level and this cap. ``"cheap"`` keeps shape and dtype checks, which run once at trace time and cost nothing in the compiled program; ``"standard"`` and above compare values, which a tracer cannot do. Parameters ---------- level : {"none", "cheap", "standard", "strict"} or None New cap. ``None`` restores the default, ``"none"``. """ global _traced_check_level _traced_check_level = normalize_check_level(level, default="none")
[docs] def get_traced_check_level() -> CheckLevel: """ Return the process-wide cap on check levels while a backend is tracing. Returns ------- CheckLevel The cap set by :func:`set_traced_check_level`; ``"none"`` by default. """ return _traced_check_level
def effective_check_level(level: CheckLevel, ops: Any) -> CheckLevel: """Return ``level``, capped by :func:`get_traced_check_level` when ``ops`` is tracing.""" if level == "none" or ops is None or not ops.is_tracing(): return level return minimum_check_level((level, _traced_check_level)) _Method = TypeVar("_Method", bound=Callable[..., Any]) # Sentinel: the core of a checked method defaults to ``_<name>_core``. _DEFAULT_CORE = object() def checked_method( *, in_space: str | None = None, out_space: str | None = None, arg_positions: int | tuple[int, ...] | None = None, in_batched: bool = False, out_batched: bool = False, out_batched_scalar: bool = False, core: str | None | object = _DEFAULT_CORE, ) -> Callable[[_Method], _Method]: """ Build a decorator that checks a method's inputs and outputs against spaces. Checks follow ``self.check_level``; at ``"none"`` the wrapper calls the method directly. Parameters ---------- in_space : str or None, optional Attribute of ``self`` holding the input space, ``"self"``, or ``None`` for no input check. out_space : str or None, optional Attribute of ``self`` holding the output space, ``"self"``, or ``None`` for no output check. arg_positions : int, tuple of int, or None, optional Positions in ``*args`` checked against ``in_space``; ``(0,)`` by default. in_batched : bool, optional Check inputs as leading-axis batches. out_batched : bool, optional Check outputs as leading-axis batches. out_batched_scalar : bool, optional Check the output has shape ``(N,)`` for an input batch of size ``N``; needs ``in_space``. core : str or None, optional The check-free core this public method calls, ``_<name>_core`` by default; ``None`` when it has none. Recorded for :func:`install_public_checks`. Returns ------- Callable[[_Method], _Method] The decorator. """ if out_batched_scalar and in_space is None: # TypeError, matching ``require_mutually_exclusive``: both are misuse of # the decorator's argument combination, not a bad runtime value. raise TypeError("out_batched_scalar requires in_space to locate the batch size.") # Precompute which arguments to check, so the wrapper stays cheap. positions = ( (0,) if arg_positions is None else (arg_positions,) if isinstance(arg_positions, int) else tuple(arg_positions) ) # Resolve positions to a single-element fast path or a tuple iteration. single_pos = positions[0] if len(positions) == 1 else None # The batch size for out_batched_scalar comes from the first checked input. batch_pos = positions[0] last_pos = max(positions) def decorate(method: _Method) -> _Method: # Parameter names after ``self``, to find a checked argument passed by keyword. names = tuple(inspect.signature(method).parameters)[1:] @wraps(method) def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: # Fast path: ``check_level == "none"`` means no validation on either side. level: CheckLevel = getattr(self, "check_level", "standard") if level == "none": return method(self, *args, **kwargs) level = effective_check_level(level, getattr(self, "ops", None)) if level == "none": return method(self, *args, **kwargs) if len(args) <= last_pos: # A checked argument came by keyword: pass the leading ones positionally. lead = [name for name in names[len(args):last_pos + 1] if name in kwargs] args = args + tuple(kwargs.pop(name) for name in lead) # Check the selected inputs: element membership, or batch membership. if in_space is not None: check_target = self if in_space == "self" else getattr(self, in_space) if in_batched: from ._batching import _check_batched if single_pos is not None: _check_batched(check_target, args[single_pos]) else: for pos in positions: _check_batched(check_target, args[pos]) else: check_member = check_target._check_member if single_pos is not None: check_member(args[single_pos]) else: for pos in positions: check_member(args[pos]) # Run the method, then check its output the same way. y = method(self, *args, **kwargs) if out_space is not None: out_target = self if out_space == "self" else getattr(self, out_space) if out_batched: from ._batching import _check_batched _check_batched(out_target, y) else: out_target._check_member(y) # A batched scalar output must have shape (N,). if out_batched_scalar and check_level_at_least(level, "standard"): from ._batching import _check_scalar_shape, _leading_batch_size batch_target = self if in_space == "self" else getattr(self, cast(str, in_space)) _check_scalar_shape(y, (_leading_batch_size(batch_target, args[batch_pos]),)) return y mark_checked(wrapper, decorate, f"_{method.__name__}_core" if core is _DEFAULT_CORE else core) return cast(_Method, wrapper) return decorate def mark_checked(wrapper: Any, rewrap: Callable[[Any], Any], core: Any) -> None: """Record on a checked public method how to re-check an override and which core it calls.""" wrapper._spacecore_checked = True wrapper._spacecore_rewrap = rewrap wrapper._spacecore_core = core def install_public_checks(cls: type) -> None: """Give each public method ``cls`` overrides the checks of the checked method it replaces. Families implement ``_*_core`` methods. A class that overrides a public checked method instead, without decorating it, has that override installed as its core and the public name restored to the base's checked method, so internal paths (which call cores) and public calls both see it. An override that calls ``super().<same name>`` cannot be a core (the base method would call it back), so it is re-checked in place; so is one whose class also defines the core, or whose base method has no core. Runs from ``ContextBound.__init_subclass__``, so it covers spaces, operators and functionals. """ for name, method in list(vars(cls).items()): if name.startswith("_") or not inspect.isfunction(method): continue if getattr(method, "_spacecore_checked", False): _restore_core_of_assigned(cls, name, method) continue for base in cls.__mro__[1:]: checked = vars(base).get(name) rewrap = getattr(checked, "_spacecore_rewrap", None) if rewrap is None: continue core = checked._spacecore_core # ``super().<name>(...)`` would reach the base method, which calls the core back. calls_super = "__class__" in method.__code__.co_freevars and name in method.__code__.co_names # Map onto a core only where the base method really delegates to one. if core is None or core in vars(cls) or calls_super or not hasattr(cls, core): setattr(cls, name, rewrap(method)) else: setattr(cls, core, method) setattr(cls, name, checked) break def _restore_core_of_assigned(cls: type, name: str, checked: Any) -> None: # ``name = Base.name`` restores Base's behaviour, so it also restores Base's core. core = getattr(checked, "_spacecore_core", None) if core is None or core in vars(cls): return # The defining class, not a subclass that merely re-installed the same wrapper. owner_name = checked.__qualname__.rpartition(".")[0] for base in cls.__mro__[1:]: if base.__qualname__ == owner_name and vars(base).get(name) is checked: if core in vars(base): setattr(cls, core, vars(base)[core]) return