Source code for sevaht_utility.hinting
"""Runtime inspection of type hints and unions.
Helpers for working with annotations at runtime: flattening unions
(:func:`iterate_types`), checking a value against an expected runtime type
(:func:`verify_type`), and extracting a callable's argument types
(:func:`get_callable_argument_hints`). These back the type-aware conversion in
:func:`sevaht_utility.parsing.csv_load`.
"""
from __future__ import annotations
from collections import deque
from dataclasses import InitVar
from inspect import signature
from types import UnionType
from typing import (
TYPE_CHECKING,
Any,
TypeVar,
Union,
get_args,
get_origin,
get_type_hints,
)
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
T = TypeVar("T")
[docs]
class InvalidTypeError(TypeError):
"""Raised when a value's type does not match the expected type."""
def __init__(
self, value: object, *, expected_type: type[T] | UnionType
) -> None:
super().__init__(
f"Expected: {expected_type}, "
f"Actual: {type(value)}, "
f"Value: {value}"
)
self.expected_type = expected_type
self.value = value
[docs]
class ParameterizedTypeNotSupportedError(TypeError):
"""Raised when verify_type receives a parameterized generic."""
def __init__(self, expected_type: object) -> None:
super().__init__(
"Parameterized generics are not supported by verify_type: "
f"{expected_type}. Use an unparameterized runtime type instead."
)
self.expected_type = expected_type
[docs]
def iterate_types(*source_types: type | UnionType) -> Iterator[type]:
"""Yield the distinct member types of one or more (possibly union) types.
Unions are flattened recursively (both ``X | Y`` and ``typing.Union``), and
duplicates are skipped, preserving first-seen order.
Args:
*source_types: Types or unions to flatten.
Yields:
Each distinct constituent type, in order.
Example:
>>> list(iterate_types(int | str))
[<class 'int'>, <class 'str'>]
"""
stack = deque(source_types)
seen: set[type] = set()
while stack:
current = stack.popleft()
if isinstance(current, UnionType) or get_origin(current) is Union:
stack.extendleft(reversed(get_args(current)))
elif current not in seen:
seen.add(current)
yield current
[docs]
def verify_type[T](expected_type: type | UnionType, value: T) -> T:
"""Return ``value`` if it matches ``expected_type``, else raise.
``expected_type`` may be a single type or a union of types; ``value``
matches if it is an instance of any member (``Any`` and ``object`` always
match). Parameterized generics such as ``list[int]`` cannot be checked at
runtime and are rejected.
Args:
expected_type: The required type or union of types.
value: The value to check.
Returns:
``value`` unchanged, for convenient inline use.
Raises:
InvalidTypeError: ``value`` matches no member of ``expected_type``.
ParameterizedTypeNotSupportedError: ``expected_type`` contains a
parameterized generic.
"""
matched = False
for candidate_type in iterate_types(expected_type):
if get_origin(candidate_type) is not None:
raise ParameterizedTypeNotSupportedError(candidate_type)
if candidate_type in (Any, object) or isinstance(
value, candidate_type
):
matched = True
if matched:
return value
raise InvalidTypeError(value, expected_type=expected_type)
[docs]
def get_callable_argument_hints(
function: Callable[..., Any],
) -> dict[str, type]:
"""Return a callable's parameters mapped to their annotated types.
The ``return`` annotation is excluded, dataclass ``InitVar`` wrappers are
unwrapped to their inner type, and unannotated parameters map to ``Any``.
Args:
function: Any callable (function, dataclass type, etc.).
Returns:
A mapping of parameter name to its type.
"""
type_hints = {
member: (
member_type
if not isinstance(member_type, InitVar)
else member_type.type
)
for member, member_type in get_type_hints(function).items()
}
return {
member: type_hints.get(member, Any)
for member in signature(function).parameters
if member != "return"
}