"""General coercion into registered view or value classes.
Two verbs share the machinery here: :func:`coerce_view` is the backend-aware, best-effort
coercion (results may be httk Views retaining their exact backend, and lossless fallbacks of
another type are allowed), while :func:`coerce` is strict (the result is a non-View instance of
the requested target, or ``TypeError``).
"""
from collections.abc import Callable, Sequence
from typing import Any, cast
from .unviewing import unview
from .view import View
[docs]
type Coercer = Callable[[Any, type], Any | None]
_coercers: list[tuple[tuple[type, ...], Coercer]] = []
[docs]
def register_coercer(coercer: Coercer, target: Any) -> None:
"""Append ``coercer`` to the registry, preserving registration order.
``target`` declares what the coercer can coerce *into*: a class, a tuple of classes, or
``typing.Any`` for a fully general coercer. During :func:`coerce`, a registered coercer is
only tried when the requested target class is a subclass of (one of) its declared targets;
``Any`` matches every target. Invalid declarations raise ``TypeError`` eagerly.
:param coercer: Conversion function to append to the registry.
:param target: Class, tuple of classes, or ``Any`` accepted by the coercer.
:raises TypeError: If ``target`` is not a class, tuple of classes, or ``Any``.
"""
if target is Any:
targets: tuple[type, ...] = (object,)
elif isinstance(target, tuple):
targets = target
else:
targets = (target,)
if not targets or not all(isinstance(entry, type) for entry in targets):
raise TypeError(f"register_coercer target must be a class, tuple of classes, or typing.Any, got {target!r}")
_coercers.append((targets, coercer))
def _try_view(view_cls: type, value: Any) -> Any | None:
"""Construct ``view_cls(value)`` and materialize it, or ``None`` if it cannot represent it.
A ``TypeError``, ``ValueError``, or ``OverflowError`` from construction means the view cannot
represent the value. If the candidate exposes ``_ensure_materialized()``, explicit coercion
calls it so lazy views are materialized and deferred data errors follow the same
fall-through contract.
"""
try:
candidate = cast(Any, view_cls)(value)
ensure_materialized = getattr(candidate, "_ensure_materialized", None)
if ensure_materialized is not None:
ensure_materialized()
return candidate
except (TypeError, ValueError, OverflowError):
return None
[docs]
def view_class_coercer(view_classes: Sequence[type]) -> Coercer:
"""Create a coercer that tries matching view classes in ``view_classes`` order.
:param view_classes: View classes to try in order.
:return: A coercer for the supplied view classes.
"""
def try_view_classes(value: Any, target: type) -> Any | None:
"""Try each compatible view class in registration order.
:param value: Value to convert.
:param target: Requested target class.
:return: The first successful view conversion, or ``None``.
"""
for cls in view_classes:
if issubclass(cls, target):
candidate = _try_view(cls, value)
if candidate is not None:
return candidate
return None
return try_view_classes
[docs]
def coerce_view(value: Any, target: Any) -> Any:
"""
Coerce ``value`` to a target class or prototype instance, backend-aware and best-effort.
The exact string ``"natural"`` is a documented sentinel that returns ``value`` unchanged.
Otherwise, a class target is used directly and an instance target is treated as a prototype,
using its type. Values already matching the target are returned unchanged — including httk
Views that subclass the target, so the exact backend is retained. A target that is a
:class:`~httk.core.views.view.View` subclass is then tried directly as a view conversion of
``value``, so any view family works without a registered coercer. Failing that, registered
coercers whose declared targets match are tried in registration order, and the first
non-``None`` result wins. If none succeeds, ``TypeError`` is raised naming the value type and
target. Coercion is best effort and favors lossless view wrapping; a coercer may return a
lossless fallback of another type (e.g. ``Fraction(1, 2)`` for target ``int``), and
individual coercers document any deliberately lossy conversion. Callers that need a plain,
exactly-typed result use :func:`coerce` instead.
:param value: Value to convert.
:param target: Target class, prototype instance, or the ``"natural"`` sentinel.
:return: The best available backend-aware conversion.
:raises TypeError: If no registered or direct conversion succeeds.
"""
if isinstance(target, str) and target == "natural":
return value
tcls = target if isinstance(target, type) else type(target)
if isinstance(value, tcls):
return value
if issubclass(tcls, View):
candidate = _try_view(tcls, value)
if candidate is not None:
return candidate
for targets, coercer in _coercers:
if not issubclass(tcls, targets):
continue
result = coercer(value, tcls)
if result is not None:
return result
raise TypeError(f"Cannot coerce {type(value)} to {target!r}")
[docs]
def coerce(value: Any, target: Any) -> Any:
"""
Coerce ``value`` strictly: return a non-View instance of the requested target or raise.
The exact string ``"natural"`` returns ``value`` unchanged (no coercion, even for a View).
Otherwise the resolution of :func:`coerce_view` applies, and then: an httk View result is
shed via :func:`~httk.core.views.unviewing.unview` unless the requested target is itself a
View class; a View result that cannot shed raises ``unview``'s own ``TypeError``; and the
final result must satisfy ``isinstance(result, target)`` — a lossless fallback of another
type (available through :func:`coerce_view`) makes strict coercion fail with ``TypeError``. An
existing non-View subtype of the target is an identity result.
:param value: Value to convert.
:param target: Target class, prototype instance, or the ``"natural"`` sentinel.
:return: A non-View instance matching the requested target.
:raises TypeError: If strict conversion cannot produce the requested target.
"""
if isinstance(target, str) and target == "natural":
return value
tcls = target if isinstance(target, type) else type(target)
result = coerce_view(value, tcls)
if isinstance(result, View) and not issubclass(tcls, View):
result = unview(result)
if not isinstance(result, tcls):
raise TypeError(f"Cannot coerce {type(value)} to {target!r}")
return result