Source code for axio.field

"""Field metadata for tool handler functions - lightweight replacement for pydantic.Field."""

from __future__ import annotations

import types
import typing
from dataclasses import dataclass
from typing import Annotated, Any, Final, Literal, get_args, get_origin

# Types for which basic isinstance checks are applied in non-strict mode.
PRIMITIVE_TYPES: frozenset[type] = frozenset({str, int, float, bool, list, dict})


[docs] def unwrap_hint(hint: Any) -> tuple[Any, bool]: """Strip ``Annotated`` and ``Optional`` wrappers. Returns ``(inner_type, is_optional)`` where *inner_type* has no Annotated or Union-with-None wrappers and *is_optional* is True when ``None`` was one of the Union members. """ inner = get_args(hint)[0] if get_origin(hint) is typing.Annotated else hint origin = get_origin(inner) if origin is types.UnionType or origin is typing.Union: inner_args = get_args(inner) non_none = [a for a in inner_args if a is not type(None)] is_optional = len(non_none) < len(inner_args) inner = non_none[0] if len(non_none) == 1 else inner return inner, is_optional return inner, False
[docs] def check_scalar(value: Any, name: str, b: type, strict: bool) -> None: """Raise TypeError when *value* does not satisfy the scalar type *b*.""" if strict: if not isinstance(value, b): raise TypeError(f"Field '{name}' requires {b.__name__}, got {type(value).__name__}") elif b is float and isinstance(value, int): pass # int is a valid JSON "number" elif b in PRIMITIVE_TYPES and value is not None and not isinstance(value, b): raise TypeError(f"Field '{name}' requires {b.__name__}, got {type(value).__name__}")
[docs] def check_list_items(value: list[Any], name: str, inner: Any) -> None: """Raise TypeError when any list element violates the generic item type.""" item_args = get_args(inner) if not item_args: return item_hint = item_args[0] item_b = bare_type(item_hint) if item_b not in PRIMITIVE_TYPES: return for idx, elem in enumerate(value): try: check_type(elem, name, item_hint, strict=False) except (TypeError, ValueError): raise TypeError(f"Field '{name}' element {idx} requires {item_b.__name__}, got {type(elem).__name__}")
[docs] def check_type(value: Any, name: str, inner: Any, *, strict: bool) -> None: """Dispatch type validation for *value* against *inner* (already unwrapped).""" origin = get_origin(inner) b = bare_type(inner) # Multi-branch union (e.g. str | int): value must satisfy at least one branch. # unwrap_hint already stripped Optional (| None), so non-None multi-unions land here. if b is object and (origin is types.UnionType or origin is typing.Union): branches = [br for br in get_args(inner) if br is not type(None)] for branch in branches: try: check_type(value, name, branch, strict=strict) return except (TypeError, ValueError): continue branch_names = " | ".join(getattr(bare_type(br), "__name__", str(br)) for br in branches) raise TypeError(f"Field '{name}' requires {branch_names}, got {type(value).__name__}") # bool is an int subclass - reject it for non-bool hints before numeric dispatch. if isinstance(value, bool) and b is not bool: raise TypeError(f"Field '{name}' requires {b.__name__}, got bool") if origin is Literal: allowed = get_args(inner) if value not in allowed: raise ValueError(f"Field '{name}' must be one of {list(allowed)!r}, got {value!r}") return check_scalar(value, name, b, strict) if origin is list and isinstance(value, list): check_list_items(value, name, inner)
[docs] class MissingSentinel: """Singleton sentinel - distinguishes 'no default' from ``None``.""" __slots__ = () def __repr__(self) -> str: return "MISSING" def __bool__(self) -> bool: return False
#: Sentinel value meaning "this field has no default and is required". MISSING: Final[MissingSentinel] = MissingSentinel()
[docs] @dataclass(frozen=True) class FieldInfo: """Metadata attached to a handler parameter via ``Annotated[T, FieldInfo(...)]``.""" description: str = "" default: Any = MISSING ge: int | float | None = None # minimum (≥) le: int | float | None = None # maximum (≤) strict: bool = False # reject implicit type coercion
[docs] def validate(self, value: Any, name: str, hint: Any) -> None: """Validate *value* against this FieldInfo's constraints, raising if invalid.""" inner, is_optional = unwrap_hint(hint) if value is None and is_optional: return check_type(value, name, inner, strict=self.strict) if self.ge is not None and value < self.ge: raise ValueError(f"Field '{name}' must be >= {self.ge}") if self.le is not None and value > self.le: raise ValueError(f"Field '{name}' must be <= {self.le}")
# noinspection PyPep8Naming
[docs] def Field( description: str = "", default: Any = MISSING, ge: int | float | None = None, le: int | float | None = None, ) -> FieldInfo: """Annotate a handler parameter with metadata (description, default, constraints). Usage:: async def search( query: Annotated[str, Field(description="Search query")], limit: Annotated[int, Field(default=10, ge=1, le=100)], ) -> str: ... """ return FieldInfo(description=description, default=default, ge=ge, le=le)
#: Drop-in replacement for ``from pydantic import StrictStr``. #: Rejects non-string values (e.g. integers) without coercion. StrictStr = Annotated[str, FieldInfo(strict=True)]
[docs] def is_classvar(annotation: Any) -> bool: """Return True if *annotation* is ``ClassVar`` or ``ClassVar[X]``.""" return get_origin(annotation) is typing.ClassVar or annotation is typing.ClassVar
[docs] def get_field_info(annotation: Any) -> FieldInfo | None: """Extract a ``FieldInfo`` from an ``Annotated`` annotation, or return ``None``.""" if get_origin(annotation) is not typing.Annotated: return None return next((a for a in get_args(annotation)[1:] if isinstance(a, FieldInfo)), None)
[docs] def bare_type(hint: Any) -> type: """Return the base Python type, stripping ``Annotated``, ``Optional``, and generic wrappers.""" origin = get_origin(hint) args = get_args(hint) if origin is typing.Annotated: return bare_type(args[0]) if origin is types.UnionType or origin is typing.Union: non_none = [a for a in args if a is not type(None)] return bare_type(non_none[0]) if len(non_none) == 1 else object if origin is not None: # Generic alias: list[int] → list, dict[str, Any] → dict, etc. return origin if isinstance(origin, type) else object return hint if isinstance(hint, type) else object