diff --git a/armarx_memory/aron/aron_dataclass.py b/armarx_memory/aron/aron_dataclass.py index c370c2abfeb6ab2581eb9313517d56f058028076..60a3c6d1635b4bc4a07797eaab9f1db2a6837fb9 100644 --- a/armarx_memory/aron/aron_dataclass.py +++ b/armarx_memory/aron/aron_dataclass.py @@ -1,3 +1,5 @@ +import logging + import dataclasses as dc import typing as ty @@ -10,34 +12,31 @@ class AronDataclass: ConversionOptions = ConversionOptions def to_dict(self) -> ty.Dict[str, ty.Any]: - from armarx_memory.aron.conversion.dataclass_from_to_pythonic import ( - dataclass_to_dict, - ) + from armarx_memory.aron.conversion import dataclass_from_to_pythonic + return dataclass_from_to_pythonic.dataclass_to_dict(self) - return dataclass_to_dict(self) - - def to_aron_ice(self) -> "armarx.aron.data.dto.Dict": - from armarx_memory.aron.conversion.dataclass_from_to_aron_ice import ( - dataclass_to_aron_ice, - ) - - return dataclass_to_aron_ice(self, options=self._get_conversion_options()) + def to_aron_ice( + self, + logger: logging.Logger = None, + ) -> "armarx.aron.data.dto.Dict": + from armarx_memory.aron.conversion import dataclass_from_to_aron_ice + return dataclass_from_to_aron_ice.dataclass_to_aron_ice(self, logger=logger) @classmethod def from_dict(cls, data: ty.Dict[str, ty.Any]) -> "AronDataclass": from armarx_memory.aron.conversion.dataclass_from_to_pythonic import ( dataclass_from_dict, ) - return dataclass_from_dict(cls, data) @classmethod - def from_aron_ice(cls, data: "armarx.aron.data.dto.Dict") -> "AronDataclass": - from armarx_memory.aron.conversion.dataclass_from_to_aron_ice import ( - dataclass_from_aron_ice, - ) - - return dataclass_from_aron_ice(cls, data, options=cls._get_conversion_options()) + def from_aron_ice( + cls, + data: "armarx.aron.data.dto.Dict", + logger: logging.Logger = None, + ) -> "AronDataclass": + from armarx_memory.aron.conversion import dataclass_from_to_aron_ice + return dataclass_from_to_aron_ice.dataclass_from_aron_ice(cls, aron=data, logger=logger) @classmethod def _get_conversion_options(cls) -> ty.Optional["ConversionOptions"]: diff --git a/armarx_memory/aron/conversion/dataclass_from_to_aron_ice.py b/armarx_memory/aron/conversion/dataclass_from_to_aron_ice.py index 139ca3570421c1a1037b4c9fb61803435ac8ce21..a5982e5b5ff0eca1043a61ab4e2beedf7b8cec9a 100644 --- a/armarx_memory/aron/conversion/dataclass_from_to_aron_ice.py +++ b/armarx_memory/aron/conversion/dataclass_from_to_aron_ice.py @@ -2,31 +2,39 @@ import typing as ty import logging -from .options import ConversionOptions - def dataclass_to_aron_ice( obj, - options: ty.Optional[ConversionOptions] = None, logger: ty.Optional[logging.Logger] = None, ) -> "armarx.aron.data.dto.GenericData": from .dataclass_from_to_pythonic import dataclass_to_dict from .pythonic_from_to_aron_ice import pythonic_to_aron_ice + if logger is not None: + logger.debug(f"Convert object of ARON dataclass {type(obj)} to Pythonic types ...") data = dataclass_to_dict(obj, logger=logger) - aron = pythonic_to_aron_ice(data, options=options) + + if logger is not None: + logger.debug("Convert Pythonic types to ARON Ice DTOs ...") + aron = pythonic_to_aron_ice(data) + return aron def dataclass_from_aron_ice( cls, aron: "armarx.aron.data.dto.GenericData", - options: ty.Optional[ConversionOptions] = None, logger: ty.Optional[logging.Logger] = None, ): from .dataclass_from_to_pythonic import dataclass_from_dict from .pythonic_from_to_aron_ice import pythonic_from_aron_ice - data = pythonic_from_aron_ice(aron, options=options) + if logger is not None: + logger.debug("Convert ARON Ice DTOs to Pythonic types ...") + data = pythonic_from_aron_ice(aron, logger=logger) + + if logger is not None: + logger.debug(f"Convert Pythonic types to ARON dataclass {cls} ...") obj = dataclass_from_dict(cls, data, logger=logger) + return obj diff --git a/armarx_memory/aron/conversion/dataclass_from_to_pythonic.py b/armarx_memory/aron/conversion/dataclass_from_to_pythonic.py index 06f154c8d2bf5cd294d728afe1a18948f227ea2c..88e9d364b984286d9709ed932448f089c7797b09 100644 --- a/armarx_memory/aron/conversion/dataclass_from_to_pythonic.py +++ b/armarx_memory/aron/conversion/dataclass_from_to_pythonic.py @@ -1,96 +1,301 @@ import logging -import dataclasses as dc import typing as ty -from .options import ConversionOptions +from armarx_memory.aron.conversion.options import ConversionOptions -def dataclass_to_dict( - obj, - logger: ty.Optional[logging.Logger] = None, -) -> ty.Dict[str, ty.Any]: - """ - Deeply converts a dataclass to a dict. +class DataclassFromToDict: - :param obj: An object of a dataclass. - :param logger: An optional logger. - :return: A dict containing pythonic data types. - """ - return dc.asdict(obj) + def __init__(self, logger: logging.Logger = None): + self.logger = logger + def _prefix(self, depth: int) -> str: + return " " * depth -def dataclass_from_dict( - cls, - data: ty.Dict, - logger: ty.Optional[logging.Logger] = None, -): - """ - Deeply converts a dictionary with pythonic data types - to an instance of the given dataclass. + def dataclass_from_dict( + self, + cls, + data: ty.Dict, + depth: int = 0, + ): + """ + Deeply converts a dictionary with pythonic data types + to an instance of the given dataclass. - :param cls: The dataclass. - :param data: The data. - :param logger: A logger. - :return: An instance of the dataclass. - """ + :param cls: The dataclass. + :param data: The data. + :param depth: The current recursion depth. Only used for logging. + :return: An instance of the dataclass. + """ + + pre = self._prefix(depth) + + if self.logger is not None: + self.logger.debug(f"{pre}Construct value of type {cls.__name__} from a {type(data)} ...") - if logger is not None: - logger.info(f"\nConstruct class {cls.__name__} from a {type(data)} ...") + if cls == type(data): + if self.logger is not None: + self.logger.debug(f"{pre}> Type matches exactly. Return data {cls.__name__} as-is.") + # Nothing to do. + return data + + from armarx_memory.aron.aron_dataclass import AronDataclass + if issubclass(cls, AronDataclass): + conversion_options = cls._get_conversion_options() + else: + conversion_options = None - try: - field_types = cls.__annotations__ - except AttributeError: + try: + field_types = cls.__annotations__ + except AttributeError: + return self.non_dataclass_from_dict(cls=cls, data=data, depth=depth) + + # Build kwargs for cls. + kwargs = dict() + for field_name, value in data.items(): + if conversion_options is not None: + field_name = conversion_options.name_aron_to_python(field_name) + + try: + field_type = field_types[field_name] + except KeyError as e: + raise KeyError( + f"Found no dataclass field '{field_name}' in ARON dataclass {cls} matching the data entry. " + "Available are: " + ", ".join(f"'{f}'" for f in field_types)) + + field_value = self.field_value_from_pythonic(field_name=field_name, field_type=field_type, value=value, depth=depth) + + kwargs[field_name] = field_value + + # Construct from kwargs and return. + return cls(**kwargs) + + def non_dataclass_from_dict(self, cls, data, depth: int): # Not a dataclass. Can just try to deliver kwargs. Or return data. + + pre = self._prefix(depth) + + method_name = "from_aron_ice" + if isinstance(cls.__dict__.get(method_name, None), classmethod): + if self.logger is not None: + self.logger.debug(f"{pre}Not a dataclass, but provides method '{method_name}()'.") + return cls.from_aron_ice(data) + try: - return cls(**data) + result = cls(**data) except TypeError: + if self.logger is not None: + self.logger.debug(f"{pre}Not a dataclass. Return data of type {type(data)} as-is..") return data + else: + if self.logger is not None: + self.logger.debug(f"{pre}Not a dataclass. Construct {cls} from kwargs.") + return result - # Build kwargs for cls. - kwargs = dict() - for field_name, value in data.items(): - field_type = field_types[field_name] + + def field_value_from_pythonic( + self, + field_name: str, + field_type, + value, + depth: int, + ): + pre = self._prefix(depth) try: origin = field_type.__origin__ except AttributeError: origin = None - if logger is not None: + if self.logger is not None: value_type = type(value) - logger.debug( - f"- Field '{field_name}' of type: {field_type}" - f"\n- origin: {origin}" - f"\n- data type: {value_type}" + self.logger.debug( + f"{pre}- Field '{field_name}':" + f"\n{pre} - type of annot.: {field_type}" + f"\n{pre} - origin: {origin}" + f"\n{pre} - type of value: {value_type}" ) - if origin in (ty.List, list): + if field_type == type(None): + if self.logger is not None: + self.logger.debug(f"{pre}> Process NoneType") + if value is None: + return None + + elif origin in (ty.List, list): [vt] = field_type.__args__ - if logger is not None: - logger.debug(f"> Process list of {vt} ") - field_value = [dataclass_from_dict(vt, v) for v in value] + if self.logger is not None: + self.logger.debug(f"{pre}> Process list of {vt} ") + return [self.dataclass_from_dict(vt, v, depth=depth+1) for v in value] elif origin in (ty.Dict, dict): kt, vt = field_type.__args__ - if logger is not None: - logger.debug(f"> Process dict {kt} -> {vt}") + if self.logger is not None: + self.logger.debug(f"{pre}> Process dict {kt} -> {vt}") if value is not None: - field_value = { - kt(k): dataclass_from_dict(vt, v) for k, v in value.items() + return { + kt(k): self.dataclass_from_dict(vt, v, depth=depth+1) for k, v in value.items() } else: - field_value = None + return None + + elif origin == ty.Union: + if self.logger is not None: + self.logger.debug(f"{pre}> Process union.") + + union_types = field_type.__args__ + for union_type in union_types: + if self.logger is not None: + self.logger.debug(f"{pre} - Try option {union_type} ...") + + result = self.dataclass_from_dict(union_type, value, depth=depth+1) + if isinstance(result, union_type): + return result else: - if logger is not None: - logger.debug(f"> Process other: {field_type}") + if self.logger is not None: + self.logger.debug(f"{pre}> Process other type: {field_type}") try: - field_value = dataclass_from_dict(field_type, value) + return self.dataclass_from_dict(field_type, value, depth=depth+1) except AttributeError: # Cannot convert. - field_value = value + if self.logger is not None: + self.logger.debug(f"{pre}> Not a dataclass. Take value {value} as-is..") + return value + + def dataclass_to_dict( + self, + obj, + depth: ty.Optional[int] = 0, + ) -> ty.Dict[str, ty.Any]: + """ + Deeply converts an ARON dataclass to a dict. + + :param obj: An object of a dataclass. + :param logger: An optional logger. + :param depth: + :return: A dict containing pythonic data types. + """ + + pre = self._prefix(depth) + + from armarx_memory.aron.aron_dataclass import AronDataclass + if isinstance(obj, AronDataclass): + conversion_options = obj._get_conversion_options() + else: + conversion_options = None + + # Does not respect conversion_options. + # dc.asdict(obj) + + if self.logger is not None: + self.logger.debug(f"{pre}Construct dictionary from object of class {obj.__class__.__name__} ...") - kwargs[field_name] = field_value + field_types = obj.__annotations__ + + # Build kwargs for cls. + data = dict() + for field_name, field_type in field_types.items(): + origin = field_type.__dict__.get("__origin__", None) + + if ty.ClassVar in [field_type, origin]: + continue + + try: + value = obj.__dict__[field_name] + except KeyError: + raise KeyError(f"Field '{field_name}' of type {field_type} (origin: {origin})" + f" not found in object of type {type(obj)}." + f" Available are: " + ", ".join(f"'{f}'" for f in obj.__dict__.keys())) + + if conversion_options is not None: + aron_field_name = conversion_options.name_python_to_aron(field_name) + else: + aron_field_name = field_name + + field_value = self.field_value_to_pythonic(name=aron_field_name, type_=field_type, value=value, depth=depth) + + data[aron_field_name] = field_value + + return data + + def field_value_to_pythonic( + self, + name: str, + type_, + value, + depth: int, + ): + pre = self._prefix(depth) + origin = type_.__dict__.get("__origin__", None) + + if self.logger is not None: + value_type = type(value) + self.logger.debug( + f"{pre}- Field '{name}':" + f"\n{pre} - type of annot.: {type_}" + f"\n{pre} - origin: {origin}" + f"\n{pre} - type of value: {value_type}" + ) + + if value is None: + if self.logger is not None: + self.logger.debug(f"{pre}> Process NoneType") + + assert type_ == type(None), type_ + return None + + elif isinstance(value, list): + if self.logger is not None: + self.logger.debug(f"{pre}> Process list ") + return [self.dataclass_to_dict(v, depth=depth+1) for v in value] + + elif isinstance(value, dict): + return {k: self.dataclass_to_dict(v, depth=depth+1) for k, v in value.items()} + + else: + if self.logger is not None: + self.logger.debug(f"{pre}> Process other type: {type_}") + try: + return self.dataclass_to_dict(value, depth=depth + 1) + except AttributeError: + # Cannot convert. + if self.logger is not None: + self.logger.debug(f"{pre}> Not a dataclass. Return value {value} as-is..") + return value + + +def dataclass_to_dict( + obj, + logger: ty.Optional[logging.Logger] = None, +) -> ty.Dict[str, ty.Any]: + """ + Deeply converts a dataclass to a dict. + + :param obj: An object of an ARON dataclass. + :param logger: An optional logger. + :return: A dict containing pythonic data types. + """ + + converter = DataclassFromToDict(logger=logger) + return converter.dataclass_to_dict(obj=obj) + + +def dataclass_from_dict( + cls, + data: ty.Dict, + logger: ty.Optional[logging.Logger] = None, +): + """ + Deeply converts a dictionary with pythonic data types + to an instance of the given ARON dataclass. + + :param cls: The dataclass. + :param data: The data. + :param logger: A logger. + :return: An instance of the dataclass. + """ - return cls(**kwargs) + converter = DataclassFromToDict(logger=logger) + return converter.dataclass_from_dict(cls=cls, data=data) diff --git a/armarx_memory/aron/conversion/options.py b/armarx_memory/aron/conversion/options.py index 2f582238e12f8d1bea44600b70e67c15b72e2fbf..c49c48a140fd7636d33ef221f1e4ebb5621d8ed8 100644 --- a/armarx_memory/aron/conversion/options.py +++ b/armarx_memory/aron/conversion/options.py @@ -12,16 +12,14 @@ class ConversionOptions: def name_python_to_aron(self, python_name: str) -> str: aron_name = self.names_python_to_aron_dict.get(python_name, None) + if aron_name is not None: + return aron_name - if aron_name is None: - if self.names_snake_case_to_camel_case: - from .name_conversion import snake_case_to_camel_case - - aron_name = snake_case_to_camel_case(python_name) - else: - aron_name = python_name + if self.names_snake_case_to_camel_case: + from .name_conversion import snake_case_to_camel_case + return snake_case_to_camel_case(python_name) - return aron_name + return python_name def name_aron_to_python(self, aron_name: str) -> str: for python, aron in self.names_python_to_aron_dict.items(): @@ -30,9 +28,6 @@ class ConversionOptions: if self.names_snake_case_to_camel_case: from .name_conversion import camel_case_to_snake_case + return camel_case_to_snake_case(aron_name) - python_name = camel_case_to_snake_case(aron_name) - else: - python_name = aron_name - - return python_name + return aron_name diff --git a/armarx_memory/aron/conversion/pythonic_from_to_aron_ice.py b/armarx_memory/aron/conversion/pythonic_from_to_aron_ice.py index fbb7b6e1137c928e4c189e8b8c35283964dc3cbc..5bc7f8add64e5352d930f0cf306876e71e83c3b4 100644 --- a/armarx_memory/aron/conversion/pythonic_from_to_aron_ice.py +++ b/armarx_memory/aron/conversion/pythonic_from_to_aron_ice.py @@ -1,15 +1,14 @@ import enum +import logging import numpy as np import typing as ty from armarx_memory.aron.aron_ice_types import AronIceTypes, dtypes_dict -from armarx_memory.aron.conversion.options import ConversionOptions def pythonic_to_aron_ice( value: ty.Any, - options: ty.Optional[ConversionOptions] = None, ) -> "armarx.aron.data.dto.GenericData": """ Deeply converts objects/values of pythonic types to their Aron Ice counterparts. @@ -37,14 +36,7 @@ def pythonic_to_aron_ice( return pythonic_to_aron_ice(value.value) # int elif isinstance(value, dict): - a = AronIceTypes.dict( - { - ( - options.name_python_to_aron(k) if options is not None else k - ): pythonic_to_aron_ice(v) - for k, v in value.items() - } - ) + a = AronIceTypes.dict({k: pythonic_to_aron_ice(v) for k, v in value.items()}) return a elif isinstance(value, AronIceTypes.Dict): @@ -63,23 +55,19 @@ def pythonic_to_aron_ice( def pythonic_from_aron_ice( data: "armarx.aron.data.dto.GenericData", - options: ty.Optional[ConversionOptions] = None, + logger: ty.Optional[logging.Logger] = None, ) -> ty.Any: """ Deeply converts an Aron data Ice object to its pythonic representation. + :param logger: :param data: The Aron data Ice object. :param options: Conversion options. :return: The pythonic representation. """ def handle_dict(elements): - return { - ( - options.name_aron_to_python(k) if options is not None else k - ): pythonic_from_aron_ice(v) - for k, v in elements.items() - } + return {k: pythonic_from_aron_ice(v) for k, v in elements.items()} def handle_list(elements): return list(map(pythonic_from_aron_ice, elements)) diff --git a/armarx_memory/core/MemoryID.py b/armarx_memory/core/MemoryID.py index 7414c8a81c3d3bd40c1e21e6ca2f059b0e029aa7..5156be5281c4d873c1a492b713b0d1aab7843ad9 100644 --- a/armarx_memory/core/MemoryID.py +++ b/armarx_memory/core/MemoryID.py @@ -225,8 +225,9 @@ class MemoryID(ice_twin.IceTwin): self.timestamp_usec = date_time_conv.from_ice(ice.timestamp) self.instance_index = ice.instanceIndex + @classmethod - def from_aron(cls, aron: "armarx.aron.data.dto.GenericData") -> "MemoryID": + def from_aron_ice(cls, aron: "armarx.aron.data.dto.GenericData") -> "MemoryID": from armarx_memory.aron.conversion import from_aron data = from_aron(aron) @@ -239,7 +240,11 @@ class MemoryID(ice_twin.IceTwin): self.instance_index = data["instanceIndex"] return self - def to_aron(self) -> "armarx.aron.data.dto.GenericData": + @classmethod + def from_aron(cls, aron: "armarx.aron.data.dto.GenericData") -> "MemoryID": + return cls.from_aron_ice(aron=aron) + + def to_aron_ice(self) -> "armarx.aron.data.dto.GenericData": import numpy as np from armarx_memory.aron.conversion import to_aron @@ -264,3 +269,7 @@ class MemoryID(ice_twin.IceTwin): "instanceIndex": self.instance_index, } return to_aron(data) + + + def to_aron(self) -> "armarx.aron.data.dto.GenericData": + return self.to_aron_ice()