from collections.abc import Callable, Mapping
from dataclasses import MISSING, Field, fields, is_dataclass
from typing import Any, ClassVar, Protocol, TypeVar
from jsonype.base_types import Json, JsonPath
from jsonype.basic_from_json_converters import (ContainedTargetType_co, FromJsonConversionError,
FromJsonConverter, ParameterizedTypeInfo,
TargetType_co)
from jsonype.basic_to_json_converters import ToJsonConverter
# Only "known" field of a dataclass
class DataClassProtocol(Protocol): # pylint: disable=too-few-public-methods
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
DataclassTarget_co = TypeVar("DataclassTarget_co", bound=DataClassProtocol, covariant=True)
DataclassTarget_contra = TypeVar("DataclassTarget_contra",
bound=DataClassProtocol, contravariant=True)
[docs]
class ToDataclass(FromJsonConverter[DataclassTarget_co, TargetType_co]):
"""Convert an object representing JSON to a :func:`dataclasses.dataclass`.
The JSON object is expected to have keys corresponding to the fields of the dataclass.
Each value is converted to the corresponding field type.
"""
[docs]
def __init__(self, strict: bool = False) -> None:
self._strict = strict
[docs]
def can_convert(
self, _js: Json, target_type_info: ParameterizedTypeInfo[Any]
) -> bool:
return is_dataclass(target_type_info.full_type)
[docs]
def convert(
self,
js: Json,
target_type_info: ParameterizedTypeInfo[DataclassTarget_co],
path: JsonPath,
from_json: Callable[[Json, type, JsonPath], ContainedTargetType_co]
) -> DataclassTarget_co:
if not isinstance(js, Mapping):
raise FromJsonConversionError(js, path, target_type_info.full_type)
if self._strict and (extra_keys := js.keys() - target_type_info.annotations.keys()):
raise FromJsonConversionError(js, path, target_type_info.full_type,
f"unexpected keys: {extra_keys}")
if missing_keys := {
field.name for field in fields(target_type_info.full_type)
if field.default == MISSING and field.default_factory == MISSING
} - js.keys():
raise FromJsonConversionError(
js, path, target_type_info.full_type, f"missing keys: {missing_keys}"
)
return target_type_info.full_type(**{
field_name: from_json(field_value,
target_type_info.annotations[field_name],
path.append(field_name))
for field_name, field_value in js.items()
if field_name in target_type_info.annotations
})
[docs]
class FromDataclass(ToJsonConverter[DataclassTarget_contra]):
"""Converts objects of :func:`dataclasses.dataclass`.
A dataclass is converted to a ``dict`` with keys corresponding to the
fields of the dataclass and values being converted with their respective
:class:`ToJsonConverter`.
"""
[docs]
def can_convert(self, o: Any) -> bool:
return is_dataclass(o) and not isinstance(o, type)
[docs]
def convert(self, o: DataclassTarget_contra, to_json: Callable[[Any], Json]) -> Json:
return {field.name: to_json(getattr(o, field.name)) for field in fields(o)}