Source code for jsondataclasses
import types
import typing
from typing import Any, Callable, Optional, TypeVar
__all__ = ("jsonfield", "jsondataclass")
_T = TypeVar("_T")
def _parse_field(
data: dict, json_key: str, field_type: type[_T], field_parser: Optional[Callable[[Any], _T]], default_value: Any
) -> _T:
type_of_type = type(field_type)
if json_key not in data and type_of_type is not typing._UnionGenericAlias and default_value is not None:
return (field_parser or field_type)(default_value)
elif type_of_type is type:
if field_parser:
return field_parser(data[json_key])
else:
return data[field_type(json_key)]
elif type_of_type is typing._UnionGenericAlias and isinstance(None, field_type.__args__[1]): # Parse Optional[Any]
return _parse_field(data, json_key, field_type.__args__[0], field_parser, default_value) if json_key in data else None
elif type_of_type is typing._LiteralGenericAlias: # Parse Literal[...]
parsed_value = _parse_field(data, json_key, type(field_type.__args__[0]), field_parser, default_value)
if parsed_value not in field_type.__args__:
raise ValueError(f"{getattr(data, json_key, default_value)} not of literal {field_type.__args__!r}")
return parsed_value
elif type_of_type is types.GenericAlias: # Parse list[Any]
return [(field_parser or field_type.__args__[0])(i) for i in data[json_key]]
[docs]def jsonfield(
key: str, parser: Optional[Callable[[Any], Any]] = None, *, default_value: Any = None
) -> tuple[str, Optional[Callable[[Any], Any]], Any]:
"""
:param key: The key in the dictionary to pick
:param parser: Function that takes in a single value and returns a value of the desired type
:param default_value: If the key is not found in the json object, the default value will be passed to the parser instead
"""
return key, parser, default_value
[docs]def jsondataclass(cls: type) -> type:
"""
Returns the same class that is passed in, with a special __init__ method added
to construct an instance of the class from a dictionary structure that complies with the class annotations ::
@jsondataclass
class Car:
colour: str = jsonfield("colour")
number_of_wheels: int = jsonfield("numberOfWheels")
"""
field_types = {k: t for k, t in cls.__annotations__.items() if not (k.startswith("__") and k.endswith("__"))}
fields = {k: v for k, v in cls.__dict__.items() if k in field_types}
def __init__(self, data: dict):
for key, field_type in field_types.items():
field_meta = fields.get(key, jsonfield(key))
setattr(
self,
key,
_parse_field(data, field_meta[0], field_type, field_meta[1], field_meta[2]),
)
def __repr__(self: cls) -> str:
return cls.__name__ + "(" + ", ".join(f"{k}={getattr(self, k)!r}" for k in fields) + ")"
cls.__init__ = __init__
cls.__repr__ = __repr__
return cls