from collections.abc import Callable, Mapping, Sequence
from types import MappingProxyType
from typing import Any, get_args, get_origin
from ._common import ExtendedType
from ._struct_like import (
Field,
NoDefault,
StructAdapterError,
StructLikeOptions,
get_fields_dataclass,
get_fields_named_tuple,
)
from ._structure import StructureHandler, StructurerContext, StructuringError
from .path import DictKey, DictValue, ListElem, PathElem, StructField, UnionVariant
[docs]
class IntoNone(StructureHandler):
"""
If ``val`` is ``None``, structures into itself,
otherwise raises a :py:class:`StructuringError`.
"""
def simple_structure(self, val: Any) -> None:
if val is not None:
raise StructuringError("The value must be `None`")
[docs]
class IntoInt(StructureHandler):
"""
If ``val`` is an ``int`` (but not ``bool``), structures into itself,
otherwise raises a :py:class:`StructuringError`.
"""
def simple_structure(self, val: Any) -> int:
# Handling a special case of `bool` here since in Python `bool` is an `int`,
# and we don't want to mix them up.
if not isinstance(val, int) or isinstance(val, bool):
raise StructuringError("The value must be an integer")
return val
[docs]
class IntoFloat(StructureHandler):
"""
If ``val`` is a ``float`` or ``int``, converts into ``float``,
otherwise raises a :py:class:`StructuringError`.
"""
def simple_structure(self, val: Any) -> float:
# Allow integers as well, even though `int` is not a subclass of `float` in Python.
if not isinstance(val, int | float):
raise StructuringError("The value must be a floating-point number")
return float(val)
[docs]
class IntoBool(StructureHandler):
"""
If ``val`` is a ``bool``, structures into itself,
otherwise raises a :py:class:`StructuringError`.
"""
def simple_structure(self, val: Any) -> bool:
if not isinstance(val, bool):
raise StructuringError("The value must be a boolean")
return val
[docs]
class IntoBytes(StructureHandler):
"""
If ``val`` is a ``bytes``, structures into itself,
otherwise raises a :py:class:`StructuringError`.
"""
def simple_structure(self, val: Any) -> bytes:
if not isinstance(val, bytes):
raise StructuringError("The value must be a bytestring")
return val
[docs]
class IntoStr(StructureHandler):
"""
If ``val`` is a ``str``, structures into itself,
otherwise raises a :py:class:`StructuringError`.
"""
def simple_structure(self, val: Any) -> str:
if not isinstance(val, str):
raise StructuringError("The value must be a string")
return val
[docs]
class IntoUnion(StructureHandler):
"""
Attempts to structure into every type in the union in order,
returns the result of the first succeeded call.
If none succeeded, raises a :py:class:`StructuringError`.
"""
def structure(self, context: StructurerContext, val: Any) -> Any:
variants = get_args(context.structure_into)
exceptions: list[tuple[PathElem, StructuringError]] = []
for variant in variants:
try:
return context.nested_structure_into(variant, val)
except StructuringError as exc: # noqa: PERF203
exceptions.append((UnionVariant(variant), exc))
raise StructuringError(f"Cannot structure into {context.structure_into}", exceptions)
[docs]
class IntoTuple(StructureHandler):
"""
Attempts to structure into a ``tuple`` given its type arguments.
If the requested type is an unqalified ``tuple``, it is treated as ``tuple[Any, ...]``.
"""
def structure(self, context: StructurerContext, val: Any) -> Any:
if not isinstance(val, Sequence):
raise StructuringError("Can only structure a `Sequence` into a tuple generic")
elem_types = get_args(context.structure_into)
# Distinguish the cases of `tuple[()]` (explicitly a tuple with 0 arguments, acceptable)
# and `tuple` (unqualified tuple, error).
if len(elem_types) == 0 and get_origin(context.structure_into) is None:
elem_types = (Any, ...)
# Homogeneous tuples (tuple[some_type, ...])
if len(elem_types) == 2 and elem_types[1] == ...:
elem_types = tuple(elem_types[0] for _ in range(len(val)))
if len(val) < len(elem_types):
raise StructuringError(
f"Not enough elements to structure into a tuple: "
f"got {len(val)}, need {len(elem_types)}"
)
if len(val) > len(elem_types):
raise StructuringError(
f"Too many elements to structure into a tuple: "
f"got {len(val)}, need {len(elem_types)}"
)
result = []
exceptions: list[tuple[PathElem, StructuringError]] = []
for index, (item, tp) in enumerate(zip(val, elem_types, strict=True)):
try:
result.append(context.nested_structure_into(tp, item))
except StructuringError as exc: # noqa: PERF203
exceptions.append((ListElem(index), exc))
if exceptions:
raise StructuringError(f"Cannot structure into {context.structure_into}", exceptions)
return tuple(result)
[docs]
class IntoList(StructureHandler):
"""
Attempts to structure into a ``list`` given its type arguments.
If the requested type is an unqalified ``list``, it is treated as ``list[Any]``.
"""
def structure(self, context: StructurerContext, val: Any) -> Any:
if not isinstance(val, Sequence):
raise StructuringError("Can only structure a `Sequence` into a list generic")
args = get_args(context.structure_into)
if len(args) == 0:
args = (Any,)
(item_type,) = args
result = []
exceptions: list[tuple[PathElem, StructuringError]] = []
for index, item in enumerate(val):
try:
result.append(context.nested_structure_into(item_type, item))
except StructuringError as exc: # noqa: PERF203
exceptions.append((ListElem(index), exc))
if exceptions:
raise StructuringError(f"Cannot structure into {context.structure_into}", exceptions)
return result
[docs]
class IntoDict(StructureHandler):
"""
Attempts to structure into a ``dict`` given its type arguments.
If the requested type is an unqalified ``dict``, it is treated as ``dict[Any, Any]``.
"""
def structure(self, context: StructurerContext, val: Any) -> Any:
if not isinstance(val, Mapping | MappingProxyType):
raise StructuringError(
"Can only structure a `Mapping` or `MappingProxyType` into a dict generic"
)
args = get_args(context.structure_into)
if len(args) == 0:
args = (Any, Any)
key_type, value_type = args
result = {}
exceptions: list[tuple[PathElem, StructuringError]] = []
for key, value in val.items():
success = True
try:
structured_key = context.nested_structure_into(key_type, key)
except StructuringError as exc:
success = False
exceptions.append((DictKey(key), exc))
try:
structured_value = context.nested_structure_into(value_type, value)
except StructuringError as exc:
success = False
exceptions.append((DictValue(key), exc))
if success:
result[structured_key] = structured_value
if exceptions:
raise StructuringError(f"Cannot structure into {context.structure_into}", exceptions)
return result
class _SequenceIntoStructLike(StructureHandler):
def __init__(
self,
get_fields: Callable[[ExtendedType[Any]], list[Field]],
options: StructLikeOptions,
):
self._get_fields = get_fields
self._options = options
def structure(self, context: StructurerContext, val: Any) -> Any:
if not isinstance(val, Sequence):
raise StructuringError(f"Can only structure a `Sequence` into {context.structure_into}")
results = {}
exceptions: list[tuple[PathElem, StructuringError]] = []
try:
struct_fields = self._get_fields(context.structure_into)
except StructAdapterError as exc:
raise StructuringError(
f"Failed to fetch field metadata for the value `{val}`: {exc}"
) from exc
if len(val) > len(struct_fields):
raise StructuringError(f"Too many fields to serialize into {context.structure_into}")
for i, field in enumerate(struct_fields[: len(val)]):
try:
results[field.name] = context.nested_structure_into(field.type, val[i])
except StructuringError as exc: # noqa: PERF203
exceptions.append((StructField(field.name), exc))
for field in struct_fields[len(val) :]:
if (
self._options.structure_fill_in_defaults
and (default := field.get_default()) is not NoDefault
):
results[field.name] = default
else:
exceptions.append((StructField(field.name), StructuringError("Missing field")))
if exceptions:
raise StructuringError(
f"Failed to structure a list into a dataclass {context.structure_into}", exceptions
)
return context.structure_into(**results)
class _MappingIntoStructLike(StructureHandler):
def __init__(
self,
get_fields: Callable[[ExtendedType[Any]], list[Field]],
options: StructLikeOptions,
):
self._get_fields = get_fields
self._options = options
def structure(self, context: StructurerContext, val: Any) -> Any:
if not isinstance(val, Mapping | MappingProxyType):
raise StructuringError(f"Can only structure a mapping into {context.structure_into}")
results = {}
exceptions: list[tuple[PathElem, StructuringError]] = []
try:
struct_fields = self._get_fields(context.structure_into)
except StructAdapterError as exc:
raise StructuringError(
f"Failed to fetch field metadata for the value `{val}`: {exc}"
) from exc
for field in struct_fields:
val_name = self._options.to_unstructured_name(field.name, field.metadata)
if val_name in val:
try:
results[field.name] = context.nested_structure_into(field.type, val[val_name])
except StructuringError as exc:
exceptions.append((StructField(field.name), exc))
continue
if (
self._options.structure_fill_in_defaults
and (default := field.get_default()) is not NoDefault
):
results[field.name] = default
else:
if val_name == field.name:
message = "Missing field"
else:
message = f"Missing field (`{val_name}` in the input)"
exceptions.append((StructField(field.name), StructuringError(message)))
if exceptions:
raise StructuringError(
f"Failed to structure a dict into {context.structure_into}", exceptions
)
return context.structure_into(**results)
[docs]
class IntoDataclassFromSequence(StructureHandler):
"""
Attempts to structure into a :py:func:`~dataclasses.dataclass` instance
from a :py:class:`~collections.abc.Sequence` type.
"""
def __init__(self, options: StructLikeOptions = StructLikeOptions()):
self._handler = _SequenceIntoStructLike(get_fields_dataclass, options)
def structure(self, context: StructurerContext, val: Any) -> Any:
return self._handler.structure(context, val)
[docs]
class IntoDataclassFromMapping(StructureHandler):
"""
Attempts to structure into a :py:func:`~dataclasses.dataclass` instance
from a :py:class:`~collections.abc.Mapping` type.
"""
def __init__(self, options: StructLikeOptions = StructLikeOptions()):
self._handler = _MappingIntoStructLike(get_fields_dataclass, options)
def structure(self, context: StructurerContext, val: Any) -> Any:
return self._handler.structure(context, val)
[docs]
class IntoNamedTupleFromSequence(StructureHandler):
"""
Attempts to structure into a :py:class:`~typing.NamedTuple` instance
from a :py:class:`~collections.abc.Sequence` type.
"""
def __init__(self, options: StructLikeOptions = StructLikeOptions()):
self._handler = _SequenceIntoStructLike(get_fields_named_tuple, options)
def structure(self, context: StructurerContext, val: Any) -> Any:
return self._handler.structure(context, val)
[docs]
class IntoNamedTupleFromMapping(StructureHandler):
"""
Attempts to structure into a :py:class:`~typing.NamedTuple` instance
from a :py:class:`~collections.abc.Mapping` type.
"""
def __init__(self, options: StructLikeOptions = StructLikeOptions()):
self._handler = _MappingIntoStructLike(get_fields_named_tuple, options)
def structure(self, context: StructurerContext, val: Any) -> Any:
return self._handler.structure(context, val)