improves SerializedObject type hint

This commit is contained in:
Mose Müller 2024-03-27 14:37:03 +01:00
parent 612e62d06b
commit 2d6c681690
2 changed files with 154 additions and 61 deletions

View File

@ -47,7 +47,7 @@ class DataServiceCache:
return { return {
"full_access_path": full_access_path, "full_access_path": full_access_path,
"value": None, "value": None,
"type": None, "type": "NoneType",
"doc": None, "doc": None,
"readonly": False, "readonly": False,
} }

View File

@ -4,12 +4,7 @@ import inspect
import logging import logging
import sys import sys
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING, Any, TypedDict, cast from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
if sys.version_info < (3, 11):
from typing_extensions import NotRequired
else:
from typing import NotRequired
import pydase.units as u import pydase.units as u
from pydase.data_service.abstract_data_service import AbstractDataService from pydase.data_service.abstract_data_service import AbstractDataService
@ -28,6 +23,10 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SerializationError(Exception):
pass
class SerializationPathError(Exception): class SerializationPathError(Exception):
pass pass
@ -41,23 +40,105 @@ class SignatureDict(TypedDict):
return_annotation: dict[str, Any] return_annotation: dict[str, Any]
SerializedObject = TypedDict( class SerializedObjectBase(TypedDict):
"SerializedObject", full_access_path: str
doc: str | None
readonly: bool
class SerializedInteger(SerializedObjectBase):
value: int
type: Literal["int"]
class SerializedFloat(SerializedObjectBase):
value: float
type: Literal["float"]
class SerializedQuantity(SerializedObjectBase):
value: u.QuantityDict
type: Literal["Quantity"]
class SerializedBool(SerializedObjectBase):
value: bool
type: Literal["bool"]
class SerializedString(SerializedObjectBase):
value: str
type: Literal["str"]
class SerializedEnum(SerializedObjectBase):
name: str
value: str
type: Literal["Enum", "ColouredEnum"]
enum: dict[str, Any]
class SerializedList(SerializedObjectBase):
value: list[SerializedObject]
type: Literal["list"]
class SerializedDict(SerializedObjectBase):
value: dict[str, SerializedObject]
type: Literal["dict"]
class SerializedNoneType(SerializedObjectBase):
value: None
type: Literal["NoneType"]
SerializedMethod = TypedDict(
"SerializedMethod",
{ {
"full_access_path": str, "full_access_path": str,
"name": NotRequired[str], "value": Literal["RUNNING"] | None,
"value": "list[SerializedObject] | float | int | str | bool | dict[str, Any] | None", # noqa: E501 "type": Literal["method"],
"type": str | None,
"doc": str | None, "doc": str | None,
"readonly": bool, "readonly": bool,
"enum": NotRequired[dict[str, Any]], "async": bool,
"async": NotRequired[bool], "signature": SignatureDict,
"signature": NotRequired[SignatureDict], "frontend_render": bool,
"frontend_render": NotRequired[bool],
}, },
) )
class SerializedException(SerializedObjectBase):
name: str
value: str
type: Literal["Exception"]
DataServiceTypes = Literal["DataService", "Image", "NumberSlider", "DeviceConnection"]
class SerializedDataService(SerializedObjectBase):
name: str
value: dict[str, SerializedObject]
type: DataServiceTypes
SerializedObject = (
SerializedBool
| SerializedFloat
| SerializedInteger
| SerializedString
| SerializedList
| SerializedDict
| SerializedNoneType
| SerializedMethod
| SerializedException
| SerializedDataService
| SerializedEnum
| SerializedQuantity
)
class Serializer: class Serializer:
@staticmethod @staticmethod
def serialize_object(obj: Any, access_path: str = "") -> SerializedObject: def serialize_object(obj: Any, access_path: str = "") -> SerializedObject:
@ -87,26 +168,41 @@ class Serializer:
elif inspect.isfunction(obj) or inspect.ismethod(obj): elif inspect.isfunction(obj) or inspect.ismethod(obj):
result = Serializer._serialize_method(obj, access_path=access_path) result = Serializer._serialize_method(obj, access_path=access_path)
else: elif isinstance(obj, int | float | bool | str | None):
obj_type = type(obj).__name__ result = Serializer._serialize_primitive(obj, access_path=access_path)
value = obj
readonly = False
doc = get_attribute_doc(obj)
result = {
"full_access_path": access_path,
"type": obj_type,
"value": value,
"readonly": readonly,
"doc": doc,
}
return result try:
return result
except UnboundLocalError:
raise SerializationError(
f"Could not serialized object of type {type(obj)}."
)
@staticmethod @staticmethod
def _serialize_exception(obj: Exception) -> SerializedObject: def _serialize_primitive(
obj: float | bool | str | None,
access_path: str,
) -> (
SerializedInteger
| SerializedFloat
| SerializedBool
| SerializedString
| SerializedNoneType
):
doc = get_attribute_doc(obj)
return { # type: ignore
"full_access_path": access_path,
"doc": doc,
"readonly": False,
"type": type(obj).__name__,
"value": obj,
}
@staticmethod
def _serialize_exception(obj: Exception) -> SerializedException:
return { return {
"full_access_path": "", "full_access_path": "",
"doc": "", "doc": None,
"readonly": True, "readonly": True,
"type": "Exception", "type": "Exception",
"value": obj.args[0], "value": obj.args[0],
@ -114,17 +210,16 @@ class Serializer:
} }
@staticmethod @staticmethod
def _serialize_enum(obj: Enum, access_path: str = "") -> SerializedObject: def _serialize_enum(obj: Enum, access_path: str = "") -> SerializedEnum:
import pydase.components.coloured_enum import pydase.components.coloured_enum
value = obj.name value = obj.name
readonly = False
doc = obj.__doc__ doc = obj.__doc__
class_name = type(obj).__name__ class_name = type(obj).__name__
if sys.version_info < (3, 11) and doc == "An enumeration.": if sys.version_info < (3, 11) and doc == "An enumeration.":
doc = None doc = None
if isinstance(obj, pydase.components.coloured_enum.ColouredEnum): if isinstance(obj, pydase.components.coloured_enum.ColouredEnum):
obj_type = "ColouredEnum" obj_type: Literal["ColouredEnum", "Enum"] = "ColouredEnum"
else: else:
obj_type = "Enum" obj_type = "Enum"
@ -133,7 +228,7 @@ class Serializer:
"name": class_name, "name": class_name,
"type": obj_type, "type": obj_type,
"value": value, "value": value,
"readonly": readonly, "readonly": False,
"doc": doc, "doc": doc,
"enum": { "enum": {
name: member.value for name, member in obj.__class__.__members__.items() name: member.value for name, member in obj.__class__.__members__.items()
@ -141,22 +236,21 @@ class Serializer:
} }
@staticmethod @staticmethod
def _serialize_quantity(obj: u.Quantity, access_path: str = "") -> SerializedObject: def _serialize_quantity(
obj_type = "Quantity" obj: u.Quantity, access_path: str = ""
readonly = False ) -> SerializedQuantity:
doc = get_attribute_doc(obj) doc = get_attribute_doc(obj)
value = {"magnitude": obj.m, "unit": str(obj.u)} value: u.QuantityDict = {"magnitude": obj.m, "unit": str(obj.u)}
return { return {
"full_access_path": access_path, "full_access_path": access_path,
"type": obj_type, "type": "Quantity",
"value": value, "value": value,
"readonly": readonly, "readonly": False,
"doc": doc, "doc": doc,
} }
@staticmethod @staticmethod
def _serialize_dict(obj: dict[str, Any], access_path: str = "") -> SerializedObject: def _serialize_dict(obj: dict[str, Any], access_path: str = "") -> SerializedDict:
obj_type = "dict"
readonly = False readonly = False
doc = get_attribute_doc(obj) doc = get_attribute_doc(obj)
value = { value = {
@ -165,15 +259,14 @@ class Serializer:
} }
return { return {
"full_access_path": access_path, "full_access_path": access_path,
"type": obj_type, "type": "dict",
"value": value, "value": value,
"readonly": readonly, "readonly": readonly,
"doc": doc, "doc": doc,
} }
@staticmethod @staticmethod
def _serialize_list(obj: list[Any], access_path: str = "") -> SerializedObject: def _serialize_list(obj: list[Any], access_path: str = "") -> SerializedList:
obj_type = "list"
readonly = False readonly = False
doc = get_attribute_doc(obj) doc = get_attribute_doc(obj)
value = [ value = [
@ -182,7 +275,7 @@ class Serializer:
] ]
return { return {
"full_access_path": access_path, "full_access_path": access_path,
"type": obj_type, "type": "list",
"value": value, "value": value,
"readonly": readonly, "readonly": readonly,
"doc": doc, "doc": doc,
@ -191,9 +284,7 @@ class Serializer:
@staticmethod @staticmethod
def _serialize_method( def _serialize_method(
obj: Callable[..., Any], access_path: str = "" obj: Callable[..., Any], access_path: str = ""
) -> SerializedObject: ) -> SerializedMethod:
obj_type = "method"
value = None
readonly = True readonly = True
doc = get_attribute_doc(obj) doc = get_attribute_doc(obj)
frontend_render = render_in_frontend(obj) frontend_render = render_in_frontend(obj)
@ -216,8 +307,8 @@ class Serializer:
return { return {
"full_access_path": access_path, "full_access_path": access_path,
"type": obj_type, "type": "method",
"value": value, "value": None,
"readonly": readonly, "readonly": readonly,
"doc": doc, "doc": doc,
"async": inspect.iscoroutinefunction(obj), "async": inspect.iscoroutinefunction(obj),
@ -228,10 +319,10 @@ class Serializer:
@staticmethod @staticmethod
def _serialize_data_service( def _serialize_data_service(
obj: AbstractDataService, access_path: str = "" obj: AbstractDataService, access_path: str = ""
) -> SerializedObject: ) -> SerializedDataService:
readonly = False readonly = False
doc = get_attribute_doc(obj) doc = get_attribute_doc(obj)
obj_type = "DataService" obj_type: DataServiceTypes = "DataService"
obj_name = obj.__class__.__name__ obj_name = obj.__class__.__name__
# Get component base class if any # Get component base class if any
@ -239,7 +330,7 @@ class Serializer:
(cls for cls in get_component_classes() if isinstance(obj, cls)), None (cls for cls in get_component_classes() if isinstance(obj, cls)), None
) )
if component_base_cls: if component_base_cls:
obj_type = component_base_cls.__name__ obj_type = component_base_cls.__name__ # type: ignore
# Get the set of DataService class attributes # Get the set of DataService class attributes
data_service_attr_set = set(dir(get_data_service_class_reference())) data_service_attr_set = set(dir(get_data_service_class_reference()))
@ -268,11 +359,13 @@ class Serializer:
val = getattr(obj, key) val = getattr(obj, key)
path = f"{access_path}.{key}" if access_path else key path = f"{access_path}.{key}" if access_path else key
value[key] = Serializer.serialize_object(val, access_path=path) serialized_object = Serializer.serialize_object(val, access_path=path)
# If there's a running task for this method # If there's a running task for this method
if key in obj._task_manager.tasks: if serialized_object["type"] == "method" and key in obj._task_manager.tasks:
value[key]["value"] = TaskStatus.RUNNING.name serialized_object["value"] = TaskStatus.RUNNING.name
value[key] = serialized_object
# If the DataService attribute is a property # If the DataService attribute is a property
if isinstance(getattr(obj.__class__, key, None), property): if isinstance(getattr(obj.__class__, key, None), property):
@ -337,7 +430,7 @@ def set_nested_value_by_path(
if next_level_serialized_object["type"] == "method": # state change of task if next_level_serialized_object["type"] == "method": # state change of task
next_level_serialized_object["value"] = ( next_level_serialized_object["value"] = (
value.name if isinstance(value, Enum) else None value.name if TaskStatus.RUNNING else None
) )
else: else:
serialized_value = dump(value) serialized_value = dump(value)
@ -349,7 +442,7 @@ def set_nested_value_by_path(
keys_to_keep = set(serialized_value.keys()) keys_to_keep = set(serialized_value.keys())
next_level_serialized_object.update(serialized_value) next_level_serialized_object.update(serialized_value) # type: ignore
# removes keys that are not present in the serialized new value # removes keys that are not present in the serialized new value
for key in list(next_level_serialized_object.keys()): for key in list(next_level_serialized_object.keys()):
@ -421,7 +514,7 @@ def get_next_level_dict_by_key(
{ {
"full_access_path": "", "full_access_path": "",
"value": None, "value": None,
"type": None, "type": "NoneType",
"doc": None, "doc": None,
"readonly": False, "readonly": False,
} }