| ## Copyright 2022 The IREE Authors | |
| # | |
| # Licensed under the Apache License v2.0 with LLVM Exceptions. | |
| # See https://llvm.org/LICENSE.txt for license information. | |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| """Helpers to serialize/deserialize objects.""" | |
| from enum import Enum | |
| from typing import Any, Dict, Optional, Sequence, Tuple, Type, TypeVar, Union | |
| import dataclasses | |
| import typing | |
| # types.NoneType is only added after Python 3.10. | |
| NONE_TYPE = type(None) | |
| SERIALIZE_FUNC_NAME = "__serialize__" | |
| DESERIALIZE_FUNC_NAME = "__deserialize__" | |
| SUPPORTED_DICT_KEY_TYPES = {str, int, float, bool} | |
| SUPPORTED_PRIMITIVE_TYPES = {str, int, float, bool, NONE_TYPE} | |
| def serialize_and_pack( | |
| obj, root_obj_field_name="root_obj", keyed_obj_map_field_name="keyed_obj_map" | |
| ): | |
| """Converts and packs the object into a serializable object. | |
| Args: | |
| obj: object to be serialized. | |
| root_obj_field_name: field name of the top-level object in the return dict. | |
| keyed_obj_map_field_name: field name of the keyed object map in the return | |
| dict. | |
| Returns | |
| A serializable dict. | |
| """ | |
| if root_obj_field_name == keyed_obj_map_field_name: | |
| raise ValueError(f"root_obj and keyed_obj_map can't have the same field name.") | |
| keyed_obj_map = {} | |
| root_obj = _serialize(obj=obj, keyed_obj_map=keyed_obj_map) | |
| return {root_obj_field_name: root_obj, keyed_obj_map_field_name: keyed_obj_map} | |
| T = TypeVar("T") | |
| def unpack_and_deserialize( | |
| data, | |
| root_type: Type[T], | |
| root_obj_field_name="root_obj", | |
| keyed_obj_map_field_name="keyed_obj_map", | |
| ) -> T: | |
| """Unpacks and deserializes the data back to the typed object. | |
| Args: | |
| data: serialized data dict. | |
| root_type: top-level object type of the data. | |
| root_obj_field_name: field name of the top-level object in the dict. | |
| keyed_obj_map_field_name: field name of the keyed object map in the dict. | |
| Returns: | |
| A deserialized object. | |
| """ | |
| obj = _deserialize( | |
| data=data[root_obj_field_name], | |
| obj_type=root_type, | |
| keyed_obj_map=data[keyed_obj_map_field_name], | |
| ) | |
| return typing.cast(root_type, obj) | |
| def _serialize(obj, keyed_obj_map: Dict[str, Any]): | |
| """Converts the object into a serializable object. | |
| Args: | |
| obj: object to be serialized. | |
| keyed_obj_map: mutable container to store the keyed serializable object. | |
| Returns | |
| A serializable object. | |
| """ | |
| serialize_func = getattr(obj, SERIALIZE_FUNC_NAME, None) | |
| if serialize_func is not None: | |
| return serialize_func(keyed_obj_map) | |
| elif isinstance(obj, list): | |
| return [_serialize(value, keyed_obj_map) for value in obj] | |
| elif isinstance(obj, Enum): | |
| return obj.name | |
| elif isinstance(obj, dict): | |
| result_dict = {} | |
| for key, value in obj.items(): | |
| if type(key) not in SUPPORTED_DICT_KEY_TYPES: | |
| raise ValueError(f"Unsupported key {key} in the dict {obj}.") | |
| result_dict[key] = _serialize(value, keyed_obj_map) | |
| return result_dict | |
| elif type(obj) in SUPPORTED_PRIMITIVE_TYPES: | |
| return obj | |
| raise ValueError(f"Unsupported object: {obj}.") | |
| def _deserialize( | |
| data, obj_type: Type, keyed_obj_map: Dict[str, Any], obj_cache: Dict[str, Any] = {} | |
| ): | |
| """Deserializes the data back to the typed object. | |
| Args: | |
| data: serialized data. | |
| obj_type: type of the data. | |
| keyed_obj_map: container of the keyed serializable object. | |
| Returns: | |
| A deserialized object. | |
| """ | |
| deserialize_func = getattr(obj_type, DESERIALIZE_FUNC_NAME, None) | |
| if deserialize_func is not None: | |
| return deserialize_func(data, keyed_obj_map, obj_cache) | |
| elif typing.get_origin(obj_type) == list: | |
| (subtype,) = typing.get_args(obj_type) | |
| return [_deserialize(item, subtype, keyed_obj_map, obj_cache) for item in data] | |
| elif typing.get_origin(obj_type) == dict: | |
| _, value_type = typing.get_args(obj_type) | |
| return dict( | |
| (key, _deserialize(value, value_type, keyed_obj_map, obj_cache)) | |
| for key, value in data.items() | |
| ) | |
| elif typing.get_origin(obj_type) == Union: | |
| subtypes = typing.get_args(obj_type) | |
| if len(subtypes) != 2 or NONE_TYPE not in subtypes: | |
| raise ValueError(f"Unsupported union type: {obj_type}.") | |
| subtype = subtypes[0] if subtypes[1] == NONE_TYPE else subtypes[1] | |
| return _deserialize(data, subtype, keyed_obj_map, obj_cache) | |
| elif issubclass(obj_type, Enum): | |
| for member in obj_type: | |
| if data == member.name: | |
| return member | |
| raise ValueError(f"Member {data} not found in the enum {obj_type}.") | |
| return data | |
| def serializable(cls=None, type_key: Optional[str] = None, id_field: str = "id"): | |
| """Decorator to make a dataclass serializable. | |
| Args: | |
| type_key: string defines the object type and indeicates that the class is a | |
| keyed object, which is unique per id and will only have one copy in the | |
| serialization per id. | |
| id_field: field name of the id field of a keyed object. | |
| Example: | |
| @serializable | |
| @dataclass | |
| class A(object): | |
| ... | |
| @serialzable(type_key="obj_b") | |
| @dataclass | |
| class B(object): | |
| id: str | |
| """ | |
| if type_key is not None and ":" in type_key: | |
| raise ValueError("':' is the reserved character in type_key.") | |
| def wrap(cls): | |
| if not dataclasses.is_dataclass(cls): | |
| raise ValueError(f"{cls} is not a dataclass.") | |
| fields = dataclasses.fields(cls) | |
| if type_key is not None and all(field.name != id_field for field in fields): | |
| raise ValueError(f'Id field "{id_field}" not found in the class {cls}.') | |
| def serialize(self, keyed_obj_map: Dict[str, Any]): | |
| if type_key is None: | |
| return _fields_to_dict(self, fields, keyed_obj_map) | |
| obj_id = getattr(self, id_field) | |
| obj_key = f"{type_key}:{obj_id}" | |
| if obj_key in keyed_obj_map: | |
| # If the value in the map is None, it means we have visited this object | |
| # before but not yet finished serializing it. This will only happen if | |
| # there is a circular reference. | |
| if keyed_obj_map[obj_key] is None: | |
| raise ValueError(f"Circular reference is not supported: {obj_key}.") | |
| return obj_id | |
| # Populate the keyed_obj_map with None first to detect circular reference. | |
| keyed_obj_map[obj_key] = None | |
| obj_dict = _fields_to_dict(self, fields, keyed_obj_map) | |
| keyed_obj_map[obj_key] = obj_dict | |
| return obj_id | |
| def deserialize(data, keyed_obj_map: Dict[str, Any], obj_cache: Dict[str, Any]): | |
| if type_key is None: | |
| field_value_map = _dict_to_fields( | |
| data, fields, keyed_obj_map, obj_cache | |
| ) | |
| return cls(**field_value_map) | |
| obj_id = data | |
| obj_key = f"{type_key}:{obj_id}" | |
| if obj_key in obj_cache: | |
| return obj_cache[obj_key] | |
| field_value_map = _dict_to_fields( | |
| keyed_obj_map[obj_key], fields, keyed_obj_map, obj_cache | |
| ) | |
| derialized_obj = cls(**field_value_map) | |
| obj_cache[obj_key] = derialized_obj | |
| return derialized_obj | |
| setattr(cls, SERIALIZE_FUNC_NAME, serialize) | |
| setattr(cls, DESERIALIZE_FUNC_NAME, deserialize) | |
| return cls | |
| # Trick to allow the decoration with `@serializable(...)`. In that case, | |
| # `serializable` is called without cls and should return a decorator. | |
| if cls is None: | |
| return wrap | |
| return wrap(cls) | |
| def _fields_to_dict( | |
| obj, fields: Sequence[dataclasses.Field], keyed_obj_map: Dict[str, Any] | |
| ) -> Dict[str, Any]: | |
| return dict( | |
| (field.name, _serialize(getattr(obj, field.name), keyed_obj_map)) | |
| for field in fields | |
| ) | |
| def _dict_to_fields( | |
| obj_dict, | |
| fields: Sequence[dataclasses.Field], | |
| keyed_obj_map: Dict[str, Any], | |
| obj_cache: Dict[str, Any], | |
| ) -> Dict[str, Any]: | |
| return dict( | |
| ( | |
| field.name, | |
| _deserialize(obj_dict[field.name], field.type, keyed_obj_map, obj_cache), | |
| ) | |
| for field in fields | |
| ) |