46 lines
1.9 KiB
Python
46 lines
1.9 KiB
Python
from dataclasses import fields, is_dataclass
|
|
from types import UnionType
|
|
from typing import Union, get_args, get_origin
|
|
|
|
|
|
def _is_optional_dataclass(field_type) -> bool:
|
|
"""
|
|
Check if the field type is an Optional containing a dataclass.
|
|
Currently, ... | None (in Python 3.10) is not supported.
|
|
"""
|
|
if get_origin(field_type) in (Union, UnionType):
|
|
inner_types = get_args(field_type)
|
|
# Check if it's a Union[Dataclass, NoneType] (i.e., Optional[Dataclass])
|
|
if len(inner_types) == 2 and any(t is type(None) for t in inner_types):
|
|
effective_type = next(t for t in get_args(field_type) if t is not type(None))
|
|
return is_dataclass(effective_type)
|
|
return False
|
|
|
|
|
|
def _hydrate_dataclass(dataclass_type, data):
|
|
"""Recursively create an instance of the dataclass_type from data."""
|
|
if not (is_dataclass(dataclass_type) or _is_optional_dataclass(dataclass_type)):
|
|
raise ValueError(f"{dataclass_type.__name__} is not a dataclass")
|
|
if data is None:
|
|
return None
|
|
|
|
field_names = {f.name: f.type for f in fields(dataclass_type)}
|
|
kwargs = {}
|
|
for key, field_type in field_names.items():
|
|
if key in data:
|
|
value = data[key]
|
|
if is_dataclass(field_type):
|
|
kwargs[key] = _hydrate_dataclass(field_type, value)
|
|
elif _is_optional_dataclass(field_type):
|
|
effective_type = next(t for t in get_args(field_type) if t is not type(None))
|
|
kwargs[key] = _hydrate_dataclass(effective_type, value)
|
|
elif get_origin(field_type) == list:
|
|
item_type = get_args(field_type)[0]
|
|
if is_dataclass(item_type):
|
|
kwargs[key] = [_hydrate_dataclass(item_type, item) for item in value]
|
|
else:
|
|
kwargs[key] = value
|
|
else:
|
|
kwargs[key] = value
|
|
return dataclass_type(**kwargs)
|