Source code for compages._unstructure
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, TypeVar
from ._common import ExtendedType, GeneratorStack, Result, get_lookup_order, isinstance_ext
from .path import PathElem
[docs]
class UnstructuringError(Exception):
"""
An error during unstructuring.
Accumulates possible nested errors.
"""
def __init__(
self, message: str, inner_errors: list[tuple[PathElem, "UnstructuringError"]] = []
):
super().__init__(message)
self.message = message
self.inner_errors = inner_errors
def __str__(self) -> str:
messages = collect_messages([], self)
_, msg = messages[0]
message_strings = [msg] + [
" " * len(path) + ".".join(str(elem) for elem in path) + f": {msg}"
for path, msg in messages[1:]
]
return "\n".join(message_strings)
def collect_messages(
path: list[PathElem], exc: UnstructuringError
) -> list[tuple[list[PathElem], str]]:
result = [(path, exc.message)]
for path_elem, inner_exc in exc.inner_errors:
result.extend(collect_messages([*path, path_elem], inner_exc))
return result
_T = TypeVar("_T")
[docs]
@dataclass
class UnstructurerContext:
"""A context object passed to handlers during unstructuring."""
unstructurer: "Unstructurer"
"""The current unstructurer."""
unstructure_as: ExtendedType[Any]
"""
The type which the value should be treated as.
Can be a regular type, a newtype, or a generic type.
"""
user_context: Any
"""
The custom object the user passed to :py:meth:`Unstructurer.unstructure_as`.
"""
[docs]
def nested_unstructure_as(self, unstructure_as: ExtendedType[_T], val: _T) -> Any:
"""
Calls :py:meth:`Unstructurer.unstructure_as` of ``self.unstructurer``
passing on the user context.
"""
return self.unstructurer.unstructure_as(unstructure_as, val, user_context=self.user_context)
[docs]
class UnstructureHandler:
"""A base class for unstructuring logic attached to a type."""
[docs]
def unstructure(
self,
context: UnstructurerContext, # noqa: ARG002
value: Any,
) -> Any:
"""
Unstructures (serializes) the given ``value`` given ``context``.
It is guaranteed that
``isinstance_ext(val, get_lookup_order(context.unstructure_as)) == True``
(see :py:func:`isinstance_ext` and :py:func:`get_lookup_order`).
"""
return self.simple_unstructure(value)
[docs]
def simple_unstructure(self, value: Any) -> Any:
"""
Unstructures the given ``value``.
Use for the cases where the information from ``context`` is not needed.
If :py:meth:`unstructure` is not defined, this method must be defined.
"""
raise NotImplementedError(
"`UnstructureHandler` must implement either `unstructure()` or `simple_unstructure()`"
)
[docs]
class Unstructurer:
def __init__(self, handlers: Mapping[Any, UnstructureHandler] = {}):
self._handlers = dict(handlers)
[docs]
def unstructure_as(
self, unstructure_as: ExtendedType[_T], val: _T, user_context: Any = None
) -> Any:
"""
Unstructures (serializes) the given ``value`` as the type ``unstructure_as``
with an optional ``user_context`` (which will be passed to the handlers).
Calls :py:func:`isinstance_ext` at the beginning,
raising :py:class:`UnstructuringError` on failure.
Raises :py:class:`UnstructuringError` for any unstructuring-related error.
"""
lookup_order = get_lookup_order(unstructure_as)
# We need this check to allow `Union` unstructuring to work
# (otherwise this check would have to be implemented manually in all handlers).
if not isinstance_ext(val, lookup_order):
# Note that `UnionType` does not have `__name__` (for whatever reason),
# but it always passes `isinstance_ext()`, so it won't appear in this branch.
raise UnstructuringError(f"The value must be of type `{unstructure_as.__name__}`")
context = UnstructurerContext(
unstructurer=self, unstructure_as=unstructure_as, user_context=user_context
)
stack = GeneratorStack[UnstructurerContext, Any](context, val)
for tp in lookup_order:
handler = self._handlers.get(tp, None)
result = stack.push(handler.unstructure if handler else None)
if result is not Result.UNDEFINED:
return result
if stack.is_empty():
raise UnstructuringError(f"No handlers registered to unstructure as {unstructure_as}")
raise UnstructuringError(
f"Could not find a non-generator handler to unstructure as {unstructure_as}"
)