import ast
import inspect
import sys
from abc import ABCMeta, abstractmethod
from typing import Dict, List, Tuple
import numpy as np
from optimeed.core.tools import rgetattr, rsetattr
[docs]MODULE_TAG = '__module__'
[docs]class SaveableObject(metaclass=ABCMeta):
"""Abstract class for dynamically type-hinted objects.
This class is to solve the special case where the exact type of an attribute is not known before runtime, yet has to be saved."""
@abstractmethod
[docs] def get_additional_attributes_to_save(self):
"""Return list of attributes corresponding to object, whose type cannot be determined statically (e.g. topology change)"""
pass
[docs]def _get_object_class(theObj):
return theObj.__class__.__qualname__
[docs]def _get_object_module(theObj):
module = theObj.__class__.__module__
if module == str.__class__.__module__:
return None
return module
[docs]def _object_to_FQCN(theobj):
"""Gets module path of object"""
module = theobj.__class__.__module__
if module is None or module == str.__class__.__module__:
return theobj.__class__.__qualname__
return module + '.' + theobj.__class__.__qualname__
#
# return theobj.__module__ + '.' + theobj.__class__.__name__
[docs]def _find_class(moduleName, className):
import importlib
splitted_name = className.split(".")
begin = True
parentClass = None
for name in splitted_name:
if begin:
parentClass = getattr(importlib.import_module(moduleName), name)
begin = False
else:
parentClass = getattr(parentClass, name)
return parentClass
[docs]def json_to_obj(json_dict):
"""Convenience class to create object from dictionary. Only works if CLASS_TAG is valid
:param json_dict: dictionary loaded from a json file.
:raise TypeError: if class can not be found
:raise KeyError: if CLASS_TAG not present in dictionary
"""
try:
cls = _find_class(json_dict[MODULE_TAG], json_dict[CLASS_TAG])
return json_to_obj_safe(json_dict, cls)
except TypeError:
raise
except KeyError:
raise KeyError("Object class not defined, can not create data. Use from_json_safe method to specify which class to load.")
[docs]def json_to_obj_safe(json_dict, cls):
"""Safe class to create object from dictionary.
:param json_dict: dictionary loaded from a json file
:param cls: class object to instantiate with dictionary
"""
if issubclass(cls, List) or issubclass(cls, Tuple):
list_type = cls.__args__[0]
instance = list()
for value in json_dict:
instance.append(json_to_obj_safe(value, list_type))
if issubclass(cls, List):
return instance
else:
return tuple(instance)
elif issubclass(cls, Dict):
key_type = cls.__args__[0]
val_type = cls.__args__[1]
instance = dict()
for key, value in json_dict.items():
instance.update({json_to_obj_safe(ast.literal_eval(key), key_type): json_to_obj_safe(value, val_type)})
return instance
elif issubclass(cls, (float, int, complex, bool, str)):
return json_dict
else: # Object must be instantiated
theInstance = _instantiates_annotated_object(json_dict, cls)
return theInstance
[docs]def _instantiates_annotated_object(_json_dict, _cls):
annotations: dict = _cls.__annotations__ if hasattr(_cls, '__annotations__') else None
# Instantiate the object
entranceParams = []
params = inspect.signature(_cls).parameters
for param in params.values():
if param.default is param.empty:
if param.annotation is not param.empty:
entranceParams.append(param.annotation())
else:
entranceParams.append(None)
else:
entranceParams.append(param.default)
# We desactivate the print during the reinstantiation of all the defaults elements:
sys.stdout = None
instance = _cls(*entranceParams)
sys.stdout = sys.__stdout__
# Set object attributes from data items
if annotations is not None:
for name, value in _json_dict.items():
if name not in EXCLUDED_TAGS:
field_type = annotations.get(name)
if inspect.isclass(field_type) and isinstance(value, (dict, tuple, list, set, frozenset)):
rsetattr(instance, name, json_to_obj_safe(value, field_type))
else:
rsetattr(instance, name, value)
if isinstance(instance, SaveableObject):
for additionalAttribute in instance.get_additional_attributes_to_save():
theSubDict = _json_dict[additionalAttribute]
try:
rsetattr(instance, additionalAttribute, json_to_obj(theSubDict))
except TypeError:
rsetattr(instance, additionalAttribute, theSubDict)
return instance
[docs]def _get_annotations(theObj):
"""Return annotated attributes"""
annotations: dict = theObj.__annotations__ if hasattr(theObj, '__annotations__') else dict()
parentClass = theObj.__mro__[1] if hasattr(theObj, '__mro__') else None
if parentClass is not None and parentClass != object:
theAnnotations = _get_annotations(parentClass)
theAnnotations.update(annotations)
return theAnnotations
return annotations
[docs]def obj_to_json(theObj):
"""Extract the json dictionary from the object. The data saved are automatically detected, using typehints.
ex: x: int=5 will be saved, x=5 won't.
Inheritance of annotation is managed by this function
"""
def _to_json(recObj, is_first=False):
if isinstance(recObj, list) or isinstance(recObj, tuple):
theList = list()
for elem in recObj:
theList.append(_to_json(elem))
return theList
elif isinstance(recObj, dict):
output_dict = dict()
for key in list(recObj):
output_dict[str(key)] = _to_json(recObj[key])
return output_dict
elif isinstance(recObj, int) or isinstance(recObj, float):
return recObj
elif isinstance(recObj, str):
return recObj
elif np.issubdtype(type(recObj), np.integer) or np.issubdtype(type(recObj), np.floating):
return recObj
elif isinstance(recObj, bool):
return str(recObj).lower()
elif recObj is None:
return "null"
else: # Item is a user-defined object
output_dict = dict()
if recObj is not None:
if is_first:
output_dict[MODULE_TAG] = _get_object_module(recObj)
output_dict[CLASS_TAG] = _get_object_class(recObj)
for attribute, is_first in _get_attributes_to_save(recObj):
try:
value = rgetattr(recObj, attribute)
output_dict[attribute] = _to_json(value, is_first=is_first)
except AttributeError:
pass
return output_dict
return _to_json(theObj, is_first=True)
[docs]def _get_attributes_to_save(theObj):
"""Return list (attribute, is_first)"""
list_1 = list()
list_2 = list()
# List 1
annotations = _get_annotations(type(theObj))
if annotations is not None:
list_1 = list(zip(annotations, [False]*len(annotations)))
# List 2
if isinstance(theObj, SaveableObject):
additional_attributes = theObj.get_additional_attributes_to_save()
list_2 = list(zip(additional_attributes, [True]*len(additional_attributes)))
return list_1 + list_2
[docs]def get_json_module_tree(theObj):
"""Return dict containing {CLASS_TAG: "class_name", MODULE_TAG: "module_name", "attribute1":{"class_name": "module_name", ...}}"""
def _nested_module_tree(recObj):
theDict = dict()
theClass = _get_object_class(recObj)
theModule = _get_object_module(recObj)
if theModule is not None:
theDict[CLASS_TAG] = theClass
theDict[MODULE_TAG] = theModule
for attribute, _ in _get_attributes_to_save(recObj):
nestedObj = rgetattr(recObj, attribute)
nested_subtree = _nested_module_tree(nestedObj)
if nested_subtree:
theDict[attribute] = nested_subtree
return theDict
return _nested_module_tree(theObj)
[docs]def encode_str_json(theStr):
return theStr.__repr__().replace("\\", "\\\\")[1:-1]
[docs]def decode_str_json(theStr):
return str(theStr.__repr__().replace("\\\\", "\\"))