"""
**File:** ``_serializable_deserialize.py``
**Region:** ``ds_common_serde_py_lib``
Description
-----------
Defines internal helpers used by ``Serializable.deserialize()`` for inspecting
dataclass fields and type hints, resolving type variables, converting field
values, and setting ``init=False`` fields after construction.
Example
-------
.. code-block:: python
from dataclasses import dataclass, field
from ds_common_serde_py_lib import Serializable
@dataclass
class WithInitFalse(Serializable):
name: str
computed: str = field(init=False, default="computed")
obj = WithInitFalse.deserialize({"name": "x"})
assert obj.computed == "computed"
"""
from __future__ import annotations
from dataclasses import MISSING
from dataclasses import fields as dc_fields
from typing import Any, TypeVar, cast, get_type_hints
from ds_common_logger_py_lib import Logger
from ._serializable_convert import _convert_value
logger = Logger.get_logger(__name__, package=True)
TypeVarType = type(TypeVar("_T_RUNTIME_MARKER_"))
T = TypeVar("T")
[docs]
def _build_type_var_map(cls: type) -> dict[Any, Any]:
"""
Build a mapping from TypeVars to their concrete types.
Args:
cls: The class to build the type var map for.
Returns:
A mapping from TypeVars to their concrete types.
"""
from typing import get_args, get_origin # noqa: PLC0415
type_var_map: dict[Any, Any] = {}
for base_alias in getattr(cls, "__orig_bases__", []) or []:
origin = get_origin(base_alias)
if origin is None:
continue
args = get_args(base_alias)
try:
params = origin.__parameters__
except (AttributeError, Exception):
continue
if params and args:
for param, arg in zip(params, args, strict=True):
type_var_map[param] = arg
return type_var_map
[docs]
def _get_class_type_hints(cls: type) -> dict[str, Any]:
"""
Get type hints from the class itself (best-effort).
Args:
cls: The class to get the type hints for.
Returns:
A dictionary of type hints.
"""
try:
return get_type_hints(cls) or {}
except Exception as exc:
logger.debug("Failed to get type hints for %s: %s", cls.__name__, exc)
return {}
[docs]
def _get_dataclass_fields(cls: type) -> tuple[Any, ...]:
"""
Get dataclass fields for the given class.
Args:
cls: The class to get the dataclass fields for.
Returns:
A tuple of dataclass fields.
"""
try:
return dc_fields(cast("Any", cls))
except Exception as exc:
raise Exception(
str(exc),
{
"type": type(exc).__name__,
"class_name": cls.__name__,
},
) from exc
[docs]
def _resolve_type_hint_for_field(
field: Any,
field_name: str,
cls_own_hints: dict[str, Any],
type_var_map: dict[Any, Any],
cls: type,
) -> Any:
"""
Resolve the type hint for a dataclass field.
Args:
field: The field to resolve the type hint for.
field_name: The name of the field.
cls_own_hints: The type hints for the class.
type_var_map: A mapping from TypeVars to their concrete types.
cls: The class to resolve the type hint for.
Returns:
The resolved type hint.
"""
if field.type and not isinstance(field.type, TypeVarType) and field.type is not Any:
hint = field.type
elif field_name in cls_own_hints:
hint = cls_own_hints[field_name]
else:
hint = field.type
if isinstance(hint, TypeVarType):
hint = type_var_map.get(hint, getattr(hint, "__bound__", Any))
if isinstance(hint, str):
try:
resolved_hints = get_type_hints(cls)
if field_name in resolved_hints:
hint = resolved_hints[field_name]
except Exception as exc:
logger.debug(
"Failed to resolve type hint '%s' for %s.%s: %s",
hint,
cls.__name__,
field_name,
exc,
)
return hint
[docs]
def _process_field(
field: Any,
raw_value: Any,
deserializers: dict[str, Any],
cls_own_hints: dict[str, Any],
type_var_map: dict[Any, Any],
cls: type,
) -> Any:
"""
Process a single field during deserialization.
Args:
field: The field to process.
raw_value: The raw value of the field.
deserializers: A dictionary of deserializers.
cls_own_hints: The type hints for the class.
type_var_map: A mapping from TypeVars to their concrete types.
cls: The class to process the field for.
Returns:
The processed value.
"""
converter = deserializers.get(field.name)
if callable(converter):
return converter(raw_value)
hint = _resolve_type_hint_for_field(
field=field,
field_name=field.name,
cls_own_hints=cls_own_hints,
type_var_map=type_var_map,
cls=cls,
)
return _convert_value(raw_value, hint)
[docs]
def _set_init_false_fields(instance: Any, class_fields: tuple[Any, ...]) -> None:
"""
Set fields with init=False and defaults after instance creation.
Args:
instance: The instance to set the fields for.
class_fields: A tuple of dataclass fields.
"""
for field in class_fields:
if not field.init and not hasattr(instance, field.name):
if field.default is not MISSING:
setattr(instance, field.name, field.default)
elif field.default_factory is not MISSING:
setattr(instance, field.name, field.default_factory())