from typing import Any class BaseStruct: """ This abstract base class helps to both model and de/serialize objects in a dict-like interface. It is similar to a python ``dataclass``, without the annoyance of needing to name properties with a leading underscore. It supports key access with dictionary syntax, e.g.: ``obj[key]``. This is a very generic abstract class that makes working with subclass objects more pleasant. :class:`BaseStruct` is useful for objects like ViewModels, that are inherently rather generic. BaseStruct is designed to be subclassed, but not created directly. Subclasses of BaseStruct can be used to abstract and model just about any object that would benefit from a middle layer between some source model or datastore, and consumers that simply need data properties (like output views) in a rather generic, but serializable format. """ def __iter__(self): return iter(vars(self)) def __setitem__(self, key: str, value: Any): """Only set properties (via string key syntax) that are defined in the class Args: key: property name value: property value """ if hasattr(self, key): setattr(self, key, value) def __getitem__(self, item): return getattr(self, item) def __len__(self): return len(vars(self)) def __str__(self): return str(self.to_dict()) @property def properties(self) -> list: """ Returns: all this object's properties that are set using the ``@property`` decorator """ return [k for k, v in vars(type(self)).items() if isinstance(v, property)] @property def properties_values(self) -> dict: """ Returns: all the property values by calling the {property} functions """ obj = {} for prop in self.properties: obj[prop] = self[prop] return obj def to_dict(self) -> dict: """ Returns: the object as dict with all attributes and properties (less any ``_ignore_fields()``) """ obj = {**vars(self), **self.properties_values} for key in self._ignore_fields(): if key in obj.keys(): obj.pop(key) return obj def to_clean_dict(self) -> dict: """ Returns: the object as dict with all attributes and properties where values are not empty """ return {k: v for k, v in self.to_dict().items() if v != ''} @classmethod def from_dict(cls, data: dict): """Simple *non-recursive* function to cast a dict to any BaseStruct child type .. code-block:: python some_struct = ChildClass.from_dict({'foo': 'bar'}) """ model = cls() condition = data and hasattr(data, 'items') and callable(data.items) if not condition: return model for key, val in data.items(): model[key] = val return model # pylint: disable=no-self-use def _ignore_fields(self): """Fields we want to omit from our serialization methods like ``.to_dict()`` Override this method in subclasses as needed. This was kept a function to avoid further complication in serialization of attributes and properties. """ return []