yggdrasil.data.cast.registry¶
registry ¶
Type conversion registry and default converters.
This module is the engine room for yggdrasil casting.
Design goals¶
Small, predictable conversion engine:
- Fast lookup for exact (from_hint, to_hint) registrations.
- MRO-aware fallback (so subclasses work without extra registrations).
- Optional[T] / T | None handling (None passes through when optional, otherwise defaults).
- Container support (list / set / tuple / dict / Mapping) via recursive element casting.
- Enum + dataclass helpers (ergonomic for schemas / config objects).
- Namespace-triggered lazy imports to register ecosystem-specific converters (polars / pandas / pyspark) only when needed.
Key ideas¶
- Converters are registered for type hints, not just raw Python types.
- We keep two registries:
_registry[(from_hint, to_hint)]for concrete registrations_any_registry[to_hint]for wildcard "Any -> to_hint" handlers- Dispatch prefers: 1) exact match 2) cheap identity 3) Any-wildcard target handler 4) MRO cross-product lookup 5) scan-based fallback (issubclass checks for "odd" keys)
Note: an earlier version of the registry auto-composed a
from -> mid -> to converter by chaining two registered hops
through any intermediate type. That path produced silent
surprises — picked an unintended intermediate when multiple
candidates matched, masked missing direct converters, and made
every cast's correctness depend on the order of unrelated
registrations. The composition step is gone; callers wanting a
two-hop cast register the direct from -> to converter
explicitly.
The public API is register_converter() + convert().
Notes on options¶
options is intentionally optional and can be:
- None
- CastOptions
- pyarrow Field / DataType / Schema
CastOptions.check_arg(...) normalizes it into a CastOptions instance, and kwargs
can override fields.
This module does not define CastOptions to avoid import cycles.
register_converter ¶
Decorator to register a converter from from_hint -> to_hint.
This preserves the original function object (and its type signature),
while registering it as a runtime Converter.
Wildcard registrations¶
If from_hint is typing.Any or object, the converter is stored in
_any_registry[to_hint] and is eligible for any source value type.
Expected converter behavior
func(value, options) -> converted_value
where options may be None.
unwrap_optional ¶
Forward to :meth:DataType.unwrap_optional.
Kept as a module-level alias for back-compat. New code reaches for
DataType.unwrap_optional directly — the canonical resolver
lives there as part of the centralized Python type-hint API.
iter_mro ¶
Yield (tp, ...) including MRO if tp is class-like; else yield (tp,).
This keeps lookups deterministic and cheap.
type_matches ¶
True if actual can use converter registered for registered.
This is slightly more permissive than plain == because it supports
issubclass checks for class-like keys.
find_converter ¶
Find the best converter for (from_type -> to_hint).
Dispatch order
1) exact (_registry[(from_type, to_hint)]) 2) identity-ish (same type, or target Any/object) 3) wildcard Any->to_hint 4) namespace-triggered late imports (polars/pandas/pyspark) once 5) MRO cross-product lookup 6) scan-based fallback with issubclass checks for odd keys
Composition (auto-chaining from -> mid -> to through any
registered intermediate) used to live as a step 7; it was
removed because the intermediate picked depended on the order
of unrelated registrations, masked missing direct converters,
and produced silent surprises. Callers needing a two-hop cast
register the direct converter explicitly.
Results (including None for "no path") are cached in _find_cache on
the check_namespace=True path so repeated calls for the same type pair pay
only a single dict lookup on subsequent invocations.
convert ¶
convert(
value: Any,
target_hint: type[T],
options: Optional[Union["CastOptions", Field, DataType, Schema]] = None,
**kwargs: Any
) -> T
Convert value to target_hint using registered converters + built-ins.
Dispatch order (cheapest first):
1) Any / object target → identity passthrough.
2) Plain-type identity: isinstance(value, target_hint) →
identity. Most calls (convert(42, int), convert('x',
str)) bail out here, before any function call.
3) Optional[T] unwrap for generic-alias targets.
4) None → None if optional, else default_scalar(target).
5) Registry lookup (exact / wildcard / namespace / MRO / scan-fallback).
6) Enum member resolution and dataclass from-mapping coercion.
7) Container generics — list / set / tuple / dict / Mapping.
8) TypeError — no path found.
Options are normalized through CastOptions.check only when the
caller actually supplied one (the no-options call site, which is the
overwhelmingly common one, skips the allocation and the import).
convert_tuple ¶
Convert an iterable into a tuple type.
Supports
- tuple[T, ...]
- tuple[T1, T2, ...] (fixed-length)
Fast path: a tuple with an Any / object element hint skips
per-element recursion.
convert_mapping ¶
convert_mapping(
value: Any, origin: Any, args: tuple[Any, ...], options: "CastOptions"
) -> Mapping[Any, Any]
Convert a mapping into dict/Mapping[K, V] with recursive casting.
Fast path: if both key and value hints are Any/object the
mapping is constructed directly without per-element recursion.
convert_to_python_enum ¶
convert_to_python_enum(
value: Any, target: type[Enum], options: Optional["CastOptions"] = None
) -> enum.Enum
Convert to an Enum member.
Strategies (all O(1) lookups via a cached per-class map):
1) Already an instance -> return as-is.
2) str -> case-insensitive lookup against member names and stringified values.
3) Otherwise -> coerce to the underlying value type of the first member, then equality-match.
convert_to_python_dataclass ¶
convert_to_python_dataclass(
value: Any, target: type[T], options: Optional["CastOptions"] = None
) -> T
Convert a mapping-like object into a dataclass instance.
Rules:
- If value is already an instance of target, return it.
- Input must be a Mapping; keys are dataclass field names.
- Fields with init=False or name starting with "_" are ignored.
- For each init field:
* if present in input -> cast using resolved type hints (get_type_hints)
* else if dataclass default/default_factory exists -> use it
* else -> default_scalar(resolved_field_type)
Why resolved_field_type matters:
- With from __future__ import annotations, dataclasses.Field.type can be a string
(e.g. "int"), which would break default_scalar(...) because it expects real hints/types.
So for defaults we must use get_type_hints(target) (already computed as hints).
convert_to_python_iterable ¶
convert_to_python_iterable(
value: Any,
origin: type,
args: tuple[Any, ...],
options: Optional["CastOptions"] = None,
) -> Any
Convert value into a list / set / frozenset with recursive casting.
Fast path: when the element hint is Any / object we skip the
per-element convert() call and materialize the container directly
from the iterator, which avoids a pure-Python loop with a function
call per element.
str_to_int ¶
Parse int from string via IntegerType. Empty string → 0.
str_to_float ¶
Parse float from string via FloatingPointType.
Honors opts.default_value when input is the empty string — convenient
for CSV/Excel ingest where missing cells round-trip as "".
str_to_bool ¶
Parse bool from string via BooleanType.
Empty string is rejected (use opts.default_value to opt in to a
fallback). The accepted truthy/falsy tokens are owned by BooleanType.