"""
Serialisation of our data models
"""
from __future__ import annotations
import copy
from pathlib import Path
from typing import Any, TypedDict, TypeVar, Union, cast
from attrs import fields
from cattrs.preconf.pyyaml import make_converter
from typing_extensions import TypeAlias
from fgen.data_models.method import Method
from fgen.data_models.module import Module
from fgen.data_models.module_enum_defining import (
EnumDefinition,
ModuleEnumDefining,
)
from fgen.data_models.multi_return import MultiReturn
from fgen.data_models.value import Value
T = TypeVar("T")
converter = make_converter(
detailed_validation=False, forbid_extra_keys=True, prefer_attrib_converters=True
)
UnstructuredElement: TypeAlias = dict[
str, Union[str, int, float, list[Any], tuple[Any, ...]]
]
UnstructuredObject: TypeAlias = Union[
UnstructuredElement, dict[str, "UnstructuredObject"]
]
def _add_name(
inp: dict[str, UnstructuredObject], cls: type[dict[str, T]]
) -> dict[str, T]:
"""
Add the name to attributes and methods automatically
This avoids having to write schemas like
.. ::code-block
Attributes
----------
k:
name: k
description: something
Methods
-------
calculate:
name: calculate
description: something else
Instead we can just write
.. ::code-block
Attributes
----------
k:
description: something
Methods
-------
calculate:
description: something else
Where the name is inferred automatically
Parameters
----------
inp
Unstructured object
cls
Type to which to structure the object
Returns
-------
Structured object
"""
value_type = cls.__args__[1] # type: ignore
res = {}
if inp is None:
raise ValueError( # noqa: TRY003 # pragma: no cover
f"Unexpected None when structuring {cls}"
)
for k, v in inp.items():
for f in fields(value_type):
if str(f.type).startswith("dict") and v.get(f.name) is None:
raise ValueError( # noqa: TRY003 # pragma: no cover
f"{f.name!r} in {k!r} is None but a dict was expected: {v}"
)
if "name" in v:
if v["name"] != k:
raise ValueError( # noqa: TRY003
f"Inconsistent name for value: {k!r} and {v['name']!r}"
)
if value_type == Method:
res[k] = converter.structure({"name": k} | v, value_type)
else:
to_structure = cast(dict[str, dict[str, str]], copy.deepcopy(v))
to_structure["definition"]["name"] = k
res[k] = converter.structure(to_structure, value_type)
return res
converter.register_structure_hook_func(
lambda t: any(
t == add_name_type
for add_name_type in (
dict[str, Value],
dict[str, MultiReturn],
dict[str, Method],
)
),
_add_name,
)
def _infer_return_type(inp: UnstructuredObject, _: Any) -> Union[Value, MultiReturn]:
if "unit" not in inp:
return converter.structure(inp, Value)
elif isinstance(inp["unit"], str):
return converter.structure(inp, Value)
elif isinstance(inp["unit"], (list, tuple)):
return converter.structure(inp, MultiReturn)
raise NotImplementedError(inp["unit"])
converter.register_structure_hook_func(
lambda t: any(
t == return_requires_inference_type
for return_requires_inference_type in [
Union[Value, MultiReturn],
]
),
_infer_return_type,
)
[docs]class UnstructuredEnumValue(TypedDict):
"""Unstructured enum value type hint"""
integer_value: int
description: str
[docs]class UnstructuredEnumDefinition(TypedDict):
"""Unstructured enum definition type hint"""
name: str
description: str
values: dict[str, UnstructuredEnumValue]
[docs]class UnstructuredEnumDefiningModule(TypedDict):
"""Unstructured enum defining module"""
name: str
description: str
provides: UnstructuredEnumDefinition
def _structure_enum_defining_module(
inp: UnstructuredEnumDefiningModule,
target_type: type[ModuleEnumDefining],
) -> ModuleEnumDefining:
injected_values = []
for k, v in inp["provides"]["values"].items():
injected_values.append({"str_value": k} | v)
injected_provides = inp["provides"] | {"values": tuple(injected_values)}
provides = converter.structure(injected_provides, EnumDefinition)
return ModuleEnumDefining(
name=inp["name"],
description=inp["description"],
provides=provides,
)
converter.register_structure_hook_func(
lambda t: t == ModuleEnumDefining,
_structure_enum_defining_module,
)
[docs]def load_module_definition(filename: str) -> Module:
"""
Read a YAML module definition file
This module definition contains a description of the Fortran module
that is being wrapped.
Parameters
----------
filename
Filename to read
Returns
-------
Loaded module definition
"""
with open(filename, encoding="utf-8") as fh:
txt = fh.read()
return converter.loads(txt, Module)
[docs]def load_enum_defining_module(file: Path) -> ModuleEnumDefining:
"""
Read a YAML enum defining module definition file
This enum defining module file contains a description of a Fortran module
that exposes an enum.
Parameters
----------
filename
Filename to read
Returns
-------
Loaded definition of the enum defining module
"""
with open(file, encoding="utf-8") as fh:
txt = fh.read()
return converter.loads(txt, ModuleEnumDefining)