Module customdataclass
This module contains a custom dataclass object.
It does work kinda good.
Expand source code
"""This module contains a custom dataclass object.
It does work kinda good.
"""
from __future__ import annotations
import types
from functools import cached_property
from typing import Any
import toml
import ujson
import yaml
class Dataclass:
"""Custom dataclass.
The real reason is that I didn't really like the way dataclasses work,
and I wanted to have a better control over the attributes.
Instead of using the decorator, the a Dataclass must be initialized
by inheriting from it and specifying the attributes in the class definition.
This simplifies the code, allowing a better control over the attributes
and the methods.
Check the examples folder for more information.
Initialization parameters:
enforce_types (bool, optional): If True, the types of the attributes
are enforced. Defaults to True.
frozen (bool, optional): If True, attributes cannot be changed after
initialization. Defaults to True
partial (bool, optional): If True, parameters can be missing in the
initialization. Defaults to False.
"""
_frozen: bool = False # the class is frozen and cannot be changed
_frozen_after_init: bool = True # the class is frozen after initialization
_enforce_types: bool = True # the types of the attributes are enforced
_partial: bool = False # the class can be initialized with missing attributes
_deserialized: bool = False # the class is being deserialized
_serializer: str | None = None # the serializer used
def __init__(self, **kwargs) -> Dataclass:
"""Create a new Dataclass.
Raises:
AttributeError: an invalid attribute is passed
AttributeError: an attribute is missing in kwargs.
TypeError: a value is not of the correct type.
"""
# unfreeze the class for the initialisation
self._frozen = False
# check if all the attributes are valid
self._checkAttributesValid(kwargs)
# set the default values
self._setDefaultValues(kwargs)
for k, v in self.__class_attributes__.items():
# skip the loop if partial is True and the attribute is not present
if self._deserialized and self._enforce_types:
# serialized format don't support tuple and set (they convert
# both to list), so we need to convert them back IMPLICITLY
if self._checkDeserializedIterator(kwargs[k], v):
# convert to tuple or set
kwargs[k] = self._deserializeOperator(kwargs[k], v)
# serialised format don't support classes (they convert them to
# dict), so we need to convert them back IMPLICITLY
elif self._checkDeserializedClass(kwargs[k], v):
# convert to class
class_type, is_list = self._getDeserializedClass(v)
kwargs[k] = self._deserializeClass(kwargs[k], class_type, is_list)
# check that the type is correct
current_value = kwargs.get(k, None)
if self._enforce_types:
correct_type = self._checkTypeCorrect(current_value, v)
else:
correct_type = True
# the type is not correct if:
# - the class is partial, the current value is not None,
# the types are enforced and the type is not correct
# - the class is not partial and the type is not correct
raise_condition = (
self._partial
and current_value is not None
and self._enforce_types
and not correct_type
) or (not self._partial and not correct_type and self._enforce_types)
if raise_condition:
types = ", ".join(t.__name__ for t in v)
raise TypeError(f"{k} should be {types}, not {current_value.__class__}")
setattr(self, k, current_value)
# freeze the class
self._frozen = self._frozen_after_init
# unset the deserialized flag
self._deserialized = False
def freeze(
self,
) -> None:
"""Freeze the class.
After this, attributes cannot be changed.
The action cannot be undone.
"""
self._frozen = True
def _checkAttributesValid(self, kwargs: dict) -> bool:
"""Check if all the attributes are valid (as specified in the class \
definition).
Args:
kwargs (dict): kwargs to check
Returns:
bool: True if all the attributes are valid, False otherwise.
"""
for k in kwargs.keys():
if k not in self.__class_attributes__.keys():
raise AttributeError(f"{k} is not a valid attribute")
return True
def _setDefaultValues(self, kwargs: dict) -> None:
"""Set the default values for the attributes.
Args:
kwargs (dict): kwargs to check
"""
for k in self.__class_attributes__:
if k not in kwargs:
try:
default_value = self.__getattribute__(k)
except AttributeError:
if self._partial:
default_value = None
else:
raise AttributeError(f"Missing {k} in kwargs")
kwargs[k] = default_value
def _checkTypeCorrect(self, value: Any, valid_type: tuple[type]) -> bool:
"""Check if the type of the value is correct.
Args:
value (Any): value to check
valid_type (tuple[type]): tuple of valid types
Returns:
bool: True if the type is correct, False otherwise.
"""
if Any in valid_type:
return True
if value is None:
return any(t == types.NoneType for t in valid_type)
def check_type(value, type: Any) -> bool:
# if the type has the __origin__ attribute, it's a generic type
if hasattr(type, "__origin__"):
if type.__origin__ is dict:
# check if the type is a dict of the specified type
for k in value:
for t in type.__args__:
if check_type(k, t):
return True
try:
# isinstance doesn't work with generic types
return isinstance(value, type)
except TypeError:
# check if the type is a tuple of the specified type
for i, t in enumerate(type.__args__):
if check_type(value[i], t):
return True
# at least one of the types must be correct
return any(check_type(value, t) for t in valid_type)
def _deserializeOperator(
self, value: list[Any], valid_type: tuple[type]
) -> set[Any] | tuple[Any] | list[Any]:
"""Return the deserialized iterator.
Args:
value (list[Any]): value to check
valid_type (type): type of the value
Returns:
bool: True if the value is valid, False otherwise.
"""
class_type = next(t for t in valid_type if t is not None)
return class_type(value)
def _checkDeserializedIterator(self, value: list[Any], valid_type: type) -> bool:
"""Check if the value is a deserialized iterator.
JSON, TOML and YAML convert sets and tuples to lists, so we need to
convert them back.
Args:
value (list[Any]): value to check
valid_type (type): type of the value
Returns:
bool: True if the value is valid, False otherwise.
"""
if isinstance(value, list):
if list in valid_type:
return False
if any(t in valid_type for t in (tuple, set)):
return True
# if value is not a list, then there's no need to convert it
return False
def _deserializeClass(
self, value: dict | list[dict], valid_type: type, is_list: bool
) -> list[Dataclass] | Dataclass:
"""Check if the value is a deserialized class.
JSON, TOML and YAML convert classes to dicts, so we need to
convert them back.
Since both lists of dictionaries and single dictionaries
can be instances of Dataclass objects (or subclasses of Dataclass),
we need to check if the value is a list or not.
Args:
value (dict | list[dict]): value to check
valid_type (type): type of the value
Returns:
bool: True if the value is valid, False otherwise.
"""
# a list of Dataclass is converted to a list
# a single Dataclass is converted to a dict
if is_list:
return [valid_type.from_dict(i) for i in value]
return valid_type.from_dict(value)
def _getDeserializedClass(self, valid_type: tuple[type]) -> tuple[type, bool]:
"""Return the deserialized class.
Objects
Args:
valid_type (tuple[type]): type of the value
Returns:
tuple[type, bool]: type of the value and if it's a list
"""
convert_class = next(t for t in valid_type if t is not None)
if hasattr(convert_class, "__origin__") and convert_class.__origin__ is list:
inner_class = next(t for t in convert_class.__args__ if t is not None)
return inner_class, True
return convert_class, False
def _checkDeserializedClass(self, value: dict, valid_type: tuple[type]) -> bool:
"""Check if the value is a valid class.
Args:
value (dict): value to check
valid_type (type): type of the value
Returns:
bool: True if the value is valid, False otherwise.
"""
# a list of Dataclass is converted to a list
# a single Dataclass is converted to a dict
if not isinstance(value, dict) and not isinstance(value, list):
return False
if isinstance(value, dict):
return any(issubclass(t, Dataclass) for t in valid_type)
if isinstance(value, list):
for i in value:
if not isinstance(i, dict):
return False
for t in valid_type:
if hasattr(t, "__origin__") and t.__origin__ is list:
return any(issubclass(t, Dataclass) for t in t.__args__)
return False
def __init_subclass__(
cls,
enforce_types: bool = True,
frozen: bool = True,
partial: bool = False,
**kwargs,
) -> None:
"""Initialize the subclass.
Args:
enforce_types (bool, optional): If True, the types of the attributes
are enforced. Defaults to True.
frozen (bool, optional): If True, attributes cannot be changed after
initialization. Defaults to True.
partial (bool, optional): If True, the class can be initialized with
missing attributes. Defaults to False.
"""
cls._enforce_types = enforce_types
cls._frozen_after_init = frozen
cls._partial = partial
super().__init_subclass__(**kwargs)
def __setattr__(self, key: str, value):
"""Set an attribute.
Args:
key (str): name of the attribute
value (any): value of the attribute
Raises:
AttributeError: Attribute is not valid
"""
if key.startswith("_"):
super().__setattr__(key, value)
return
if self._frozen:
raise AttributeError(
f"Can't set {key}. {self.__class__.__name__} is immutable."
)
super().__setattr__(key, value)
def __repr__(self) -> str:
"""Return a string representation of the object.
Returns:
str
"""
if not self.__clean_dict__:
return f"{self.__class__.__name__}()"
parentheses = {
"tuple": ("(", ")"),
"list": ("[", "]"),
"set": ("{", "}"),
"dict": ("{", "}"),
}
s = f"{self.__class__.__name__}("
for k, v in self.__clean_dict__.items():
if self._partial and v is None:
continue
s += f"{k}="
if isinstance(v, str):
s += f'"{v}"'
elif isinstance(v, (list, tuple, set, dict)):
s += parentheses[v.__class__.__name__][0]
if isinstance(v, dict):
s += ", ".join(f'"{k}": {v}' for k, v in v.items())
else:
s += f"{', '.join(str(i) for i in v)}"
s += parentheses[v.__class__.__name__][1]
else:
s += str(v)
s += ", "
s = s[:-2] + ")"
return s
def __str__(self) -> str:
"""Return a string representation of the object.
Returns:
str
"""
return self.__repr__()
def __eq__(self, other) -> bool:
"""Compare two objects.
Args:
other (any): object to compare
Returns:
bool
"""
if not isinstance(other, self.__class__):
return False
for k in self.__class_attributes__:
if getattr(self, k) != getattr(other, k):
return False
return True
def __hash__(self) -> int:
"""Return the hash of the object.
Returns:
int
"""
ordered = sorted(self.__clean_dict__.items())
return hash(tuple(ordered))
def __contains__(self, item) -> bool:
"""Check if the object contains an item.
This is used to check if an attribute exists via the
built-in `in` operator.
Args:
item (any): item to check
Returns:
bool
"""
return item in self.__clean_dict__.keys()
def __iter__(self):
"""Return an iterator for the object.
Returns:
iterator
"""
return iter(self.__clean_dict__.items())
@cached_property
def __class_attributes__(self) -> dict[str, type]:
"""Return all the attributes of the class and their type.
Returns:
dict
"""
return self._loadAnnotationsIterative()
def _loadAnnotationsIterative(
self,
current: dict[str, type] = None,
annotations: dict[str, type] = None,
cls: type = None,
) -> dict[str, type]:
"""Load the annotations of the class and its parents.
Args:
current (dict[str, type], optional): current annotations. Defaults to None.
annotations (dict[str, type], optional): annotations of the current class. \
Defaults to None.
cls (type, optional): current class. Defaults to None.
Returns:
dict[str, type]
"""
if current is None:
current = dict()
if annotations is None:
annotations = self.__annotations__
if cls is None:
cls = self.__class__
current.update(self._extractAnnotations(annotations))
for p in cls.__bases__:
if issubclass(p, Dataclass) and p is not Dataclass:
current = self._loadAnnotationsIterative(current, p.__annotations__, p)
return current
def _extractAnnotations(self, annotations: dict[str, type]) -> dict[str, type]:
current = dict()
for k, v in annotations.items():
if isinstance(v, str):
continue
if k in current:
continue
if v is Any:
current[k] = (Any,)
elif isinstance(v, types.UnionType):
current[k] = tuple(t for t in v.__args__)
else:
current[k] = (v,)
return current
@property
def __clean_dict__(self) -> dict:
"""Return a dictionary with all the attributes of the object, \
except for the ones starting with an underscore (private).
Returns:
dict
"""
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
def _importDecorator(f, *_, **__) -> None:
"""Import the correct serializer for the function.
The serializer will be put in the `_serializer` attribute of the object.
It's mandatory to have all the functions decorated with this decorator
to contain the name of the serializer in their name.
Unittest will require all the serializers to be installed.
Args:
f (function): function to decorate
Raises:
ImportError: Could not import the correct serializer
"""
libs = {
"json": ujson, # could be ujson but json is in the stdlib
"yaml": yaml, # not in the stdlib
"toml": toml, # not in the stdlib
}
serializer = None
for k, v in libs.items():
if k in f.__name__:
serializer = v
break
def wrapper(self: Dataclass, *args, **kwargs):
self._serializer = serializer
return f(self, *args, **kwargs)
return wrapper
@property
def to_dict(self) -> dict:
"""Return a dictionary with all the attributes of the object.
Returns:
dict
"""
def iterable_type(var: any) -> type:
if isinstance(var, (list, tuple, set)):
return var.__class__
return None
d = {}
for k, v in self.__clean_dict__.items():
if t := iterable_type(v):
# handle recursive lists
d[k] = t(i.to_dict if isinstance(i, Dataclass) else i for i in v)
elif isinstance(v, Dataclass):
# handle recursive dataclasses
d[k] = v.to_dict
else:
# simple types
d[k] = v
return d
@property
def frozen(self) -> bool:
"""Return the frozen status of the object."""
return self._frozen
@property
@_importDecorator
def to_json(self) -> str:
"""
Return a json representation of the object.
Attributes are recursively converted to json.
Returns:
str
"""
dict_data = self.to_dict
# all the sets and tuples are converted to lists
# because json doesn't support them
for k, v in dict_data.items():
if isinstance(v, (set, tuple)):
dict_data[k] = list(v)
return self._serializer.dumps(dict_data)
@property
def to_json_pretty(self) -> str:
"""Return a pretty json representation of the object.
Returns:
str
"""
return self._serializer.dumps(self._serializer.loads(self.to_json), indent=4)
@property
@_importDecorator
def to_toml(self) -> str:
"""Return a toml representation of the object.
Returns:
str
"""
return self._serializer.dumps(self.to_dict)
@property
@_importDecorator
def to_yaml(self) -> str:
"""Return a yaml representation of the object.
Returns:
str
"""
return self._serializer.dump(self.to_dict)
@property
def attributes(self) -> list:
"""Return a list of all the attributes of the class.
Returns:
list
"""
return list(self.__class_attributes__.keys())
@classmethod
@_importDecorator
def from_json(cls, json_string: str) -> Dataclass:
"""Create an object from a json string.
Args:
json_string (str): json string
Returns:
Dataclass
"""
cls._deserialized = True
return cls(**cls._serializer.loads(json_string))
@classmethod
@_importDecorator
def from_toml(cls, toml_string: str) -> Dataclass:
"""Create an object from a toml string.
Args:
toml_string (str): toml string
Returns:
Dataclass
"""
cls._deserialized = True
return cls(**cls._serializer.loads(toml_string))
@classmethod
@_importDecorator
def from_yaml(cls, yaml_string: str) -> Dataclass:
"""Create an object from a yaml string.
Args:
yaml_string (str): yaml string
Returns:
Dataclass
"""
cls._deserialized = True
return cls(
**cls._serializer.load(yaml_string, Loader=cls._serializer.FullLoader)
)
@classmethod
def from_dict(cls, d: dict) -> Dataclass:
"""Create an object from a dictionary.
Args:
d (dict): dictionary
Returns:
Dataclass
"""
cls._deserialized = True
return cls(**d)
Classes
class Dataclass (**kwargs)
-
Custom dataclass.
The real reason is that I didn't really like the way dataclasses work, and I wanted to have a better control over the attributes.
Instead of using the decorator, the a Dataclass must be initialized by inheriting from it and specifying the attributes in the class definition.
This simplifies the code, allowing a better control over the attributes and the methods.
Check the examples folder for more information.
Initialization parameters: enforce_types (bool, optional): If True, the types of the attributes are enforced. Defaults to True. frozen (bool, optional): If True, attributes cannot be changed after initialization. Defaults to True partial (bool, optional): If True, parameters can be missing in the initialization. Defaults to False.
Create a new Dataclass.
Raises
AttributeError
- an invalid attribute is passed
AttributeError
- an attribute is missing in kwargs.
TypeError
- a value is not of the correct type.
Expand source code
class Dataclass: """Custom dataclass. The real reason is that I didn't really like the way dataclasses work, and I wanted to have a better control over the attributes. Instead of using the decorator, the a Dataclass must be initialized by inheriting from it and specifying the attributes in the class definition. This simplifies the code, allowing a better control over the attributes and the methods. Check the examples folder for more information. Initialization parameters: enforce_types (bool, optional): If True, the types of the attributes are enforced. Defaults to True. frozen (bool, optional): If True, attributes cannot be changed after initialization. Defaults to True partial (bool, optional): If True, parameters can be missing in the initialization. Defaults to False. """ _frozen: bool = False # the class is frozen and cannot be changed _frozen_after_init: bool = True # the class is frozen after initialization _enforce_types: bool = True # the types of the attributes are enforced _partial: bool = False # the class can be initialized with missing attributes _deserialized: bool = False # the class is being deserialized _serializer: str | None = None # the serializer used def __init__(self, **kwargs) -> Dataclass: """Create a new Dataclass. Raises: AttributeError: an invalid attribute is passed AttributeError: an attribute is missing in kwargs. TypeError: a value is not of the correct type. """ # unfreeze the class for the initialisation self._frozen = False # check if all the attributes are valid self._checkAttributesValid(kwargs) # set the default values self._setDefaultValues(kwargs) for k, v in self.__class_attributes__.items(): # skip the loop if partial is True and the attribute is not present if self._deserialized and self._enforce_types: # serialized format don't support tuple and set (they convert # both to list), so we need to convert them back IMPLICITLY if self._checkDeserializedIterator(kwargs[k], v): # convert to tuple or set kwargs[k] = self._deserializeOperator(kwargs[k], v) # serialised format don't support classes (they convert them to # dict), so we need to convert them back IMPLICITLY elif self._checkDeserializedClass(kwargs[k], v): # convert to class class_type, is_list = self._getDeserializedClass(v) kwargs[k] = self._deserializeClass(kwargs[k], class_type, is_list) # check that the type is correct current_value = kwargs.get(k, None) if self._enforce_types: correct_type = self._checkTypeCorrect(current_value, v) else: correct_type = True # the type is not correct if: # - the class is partial, the current value is not None, # the types are enforced and the type is not correct # - the class is not partial and the type is not correct raise_condition = ( self._partial and current_value is not None and self._enforce_types and not correct_type ) or (not self._partial and not correct_type and self._enforce_types) if raise_condition: types = ", ".join(t.__name__ for t in v) raise TypeError(f"{k} should be {types}, not {current_value.__class__}") setattr(self, k, current_value) # freeze the class self._frozen = self._frozen_after_init # unset the deserialized flag self._deserialized = False def freeze( self, ) -> None: """Freeze the class. After this, attributes cannot be changed. The action cannot be undone. """ self._frozen = True def _checkAttributesValid(self, kwargs: dict) -> bool: """Check if all the attributes are valid (as specified in the class \ definition). Args: kwargs (dict): kwargs to check Returns: bool: True if all the attributes are valid, False otherwise. """ for k in kwargs.keys(): if k not in self.__class_attributes__.keys(): raise AttributeError(f"{k} is not a valid attribute") return True def _setDefaultValues(self, kwargs: dict) -> None: """Set the default values for the attributes. Args: kwargs (dict): kwargs to check """ for k in self.__class_attributes__: if k not in kwargs: try: default_value = self.__getattribute__(k) except AttributeError: if self._partial: default_value = None else: raise AttributeError(f"Missing {k} in kwargs") kwargs[k] = default_value def _checkTypeCorrect(self, value: Any, valid_type: tuple[type]) -> bool: """Check if the type of the value is correct. Args: value (Any): value to check valid_type (tuple[type]): tuple of valid types Returns: bool: True if the type is correct, False otherwise. """ if Any in valid_type: return True if value is None: return any(t == types.NoneType for t in valid_type) def check_type(value, type: Any) -> bool: # if the type has the __origin__ attribute, it's a generic type if hasattr(type, "__origin__"): if type.__origin__ is dict: # check if the type is a dict of the specified type for k in value: for t in type.__args__: if check_type(k, t): return True try: # isinstance doesn't work with generic types return isinstance(value, type) except TypeError: # check if the type is a tuple of the specified type for i, t in enumerate(type.__args__): if check_type(value[i], t): return True # at least one of the types must be correct return any(check_type(value, t) for t in valid_type) def _deserializeOperator( self, value: list[Any], valid_type: tuple[type] ) -> set[Any] | tuple[Any] | list[Any]: """Return the deserialized iterator. Args: value (list[Any]): value to check valid_type (type): type of the value Returns: bool: True if the value is valid, False otherwise. """ class_type = next(t for t in valid_type if t is not None) return class_type(value) def _checkDeserializedIterator(self, value: list[Any], valid_type: type) -> bool: """Check if the value is a deserialized iterator. JSON, TOML and YAML convert sets and tuples to lists, so we need to convert them back. Args: value (list[Any]): value to check valid_type (type): type of the value Returns: bool: True if the value is valid, False otherwise. """ if isinstance(value, list): if list in valid_type: return False if any(t in valid_type for t in (tuple, set)): return True # if value is not a list, then there's no need to convert it return False def _deserializeClass( self, value: dict | list[dict], valid_type: type, is_list: bool ) -> list[Dataclass] | Dataclass: """Check if the value is a deserialized class. JSON, TOML and YAML convert classes to dicts, so we need to convert them back. Since both lists of dictionaries and single dictionaries can be instances of Dataclass objects (or subclasses of Dataclass), we need to check if the value is a list or not. Args: value (dict | list[dict]): value to check valid_type (type): type of the value Returns: bool: True if the value is valid, False otherwise. """ # a list of Dataclass is converted to a list # a single Dataclass is converted to a dict if is_list: return [valid_type.from_dict(i) for i in value] return valid_type.from_dict(value) def _getDeserializedClass(self, valid_type: tuple[type]) -> tuple[type, bool]: """Return the deserialized class. Objects Args: valid_type (tuple[type]): type of the value Returns: tuple[type, bool]: type of the value and if it's a list """ convert_class = next(t for t in valid_type if t is not None) if hasattr(convert_class, "__origin__") and convert_class.__origin__ is list: inner_class = next(t for t in convert_class.__args__ if t is not None) return inner_class, True return convert_class, False def _checkDeserializedClass(self, value: dict, valid_type: tuple[type]) -> bool: """Check if the value is a valid class. Args: value (dict): value to check valid_type (type): type of the value Returns: bool: True if the value is valid, False otherwise. """ # a list of Dataclass is converted to a list # a single Dataclass is converted to a dict if not isinstance(value, dict) and not isinstance(value, list): return False if isinstance(value, dict): return any(issubclass(t, Dataclass) for t in valid_type) if isinstance(value, list): for i in value: if not isinstance(i, dict): return False for t in valid_type: if hasattr(t, "__origin__") and t.__origin__ is list: return any(issubclass(t, Dataclass) for t in t.__args__) return False def __init_subclass__( cls, enforce_types: bool = True, frozen: bool = True, partial: bool = False, **kwargs, ) -> None: """Initialize the subclass. Args: enforce_types (bool, optional): If True, the types of the attributes are enforced. Defaults to True. frozen (bool, optional): If True, attributes cannot be changed after initialization. Defaults to True. partial (bool, optional): If True, the class can be initialized with missing attributes. Defaults to False. """ cls._enforce_types = enforce_types cls._frozen_after_init = frozen cls._partial = partial super().__init_subclass__(**kwargs) def __setattr__(self, key: str, value): """Set an attribute. Args: key (str): name of the attribute value (any): value of the attribute Raises: AttributeError: Attribute is not valid """ if key.startswith("_"): super().__setattr__(key, value) return if self._frozen: raise AttributeError( f"Can't set {key}. {self.__class__.__name__} is immutable." ) super().__setattr__(key, value) def __repr__(self) -> str: """Return a string representation of the object. Returns: str """ if not self.__clean_dict__: return f"{self.__class__.__name__}()" parentheses = { "tuple": ("(", ")"), "list": ("[", "]"), "set": ("{", "}"), "dict": ("{", "}"), } s = f"{self.__class__.__name__}(" for k, v in self.__clean_dict__.items(): if self._partial and v is None: continue s += f"{k}=" if isinstance(v, str): s += f'"{v}"' elif isinstance(v, (list, tuple, set, dict)): s += parentheses[v.__class__.__name__][0] if isinstance(v, dict): s += ", ".join(f'"{k}": {v}' for k, v in v.items()) else: s += f"{', '.join(str(i) for i in v)}" s += parentheses[v.__class__.__name__][1] else: s += str(v) s += ", " s = s[:-2] + ")" return s def __str__(self) -> str: """Return a string representation of the object. Returns: str """ return self.__repr__() def __eq__(self, other) -> bool: """Compare two objects. Args: other (any): object to compare Returns: bool """ if not isinstance(other, self.__class__): return False for k in self.__class_attributes__: if getattr(self, k) != getattr(other, k): return False return True def __hash__(self) -> int: """Return the hash of the object. Returns: int """ ordered = sorted(self.__clean_dict__.items()) return hash(tuple(ordered)) def __contains__(self, item) -> bool: """Check if the object contains an item. This is used to check if an attribute exists via the built-in `in` operator. Args: item (any): item to check Returns: bool """ return item in self.__clean_dict__.keys() def __iter__(self): """Return an iterator for the object. Returns: iterator """ return iter(self.__clean_dict__.items()) @cached_property def __class_attributes__(self) -> dict[str, type]: """Return all the attributes of the class and their type. Returns: dict """ return self._loadAnnotationsIterative() def _loadAnnotationsIterative( self, current: dict[str, type] = None, annotations: dict[str, type] = None, cls: type = None, ) -> dict[str, type]: """Load the annotations of the class and its parents. Args: current (dict[str, type], optional): current annotations. Defaults to None. annotations (dict[str, type], optional): annotations of the current class. \ Defaults to None. cls (type, optional): current class. Defaults to None. Returns: dict[str, type] """ if current is None: current = dict() if annotations is None: annotations = self.__annotations__ if cls is None: cls = self.__class__ current.update(self._extractAnnotations(annotations)) for p in cls.__bases__: if issubclass(p, Dataclass) and p is not Dataclass: current = self._loadAnnotationsIterative(current, p.__annotations__, p) return current def _extractAnnotations(self, annotations: dict[str, type]) -> dict[str, type]: current = dict() for k, v in annotations.items(): if isinstance(v, str): continue if k in current: continue if v is Any: current[k] = (Any,) elif isinstance(v, types.UnionType): current[k] = tuple(t for t in v.__args__) else: current[k] = (v,) return current @property def __clean_dict__(self) -> dict: """Return a dictionary with all the attributes of the object, \ except for the ones starting with an underscore (private). Returns: dict """ return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} def _importDecorator(f, *_, **__) -> None: """Import the correct serializer for the function. The serializer will be put in the `_serializer` attribute of the object. It's mandatory to have all the functions decorated with this decorator to contain the name of the serializer in their name. Unittest will require all the serializers to be installed. Args: f (function): function to decorate Raises: ImportError: Could not import the correct serializer """ libs = { "json": ujson, # could be ujson but json is in the stdlib "yaml": yaml, # not in the stdlib "toml": toml, # not in the stdlib } serializer = None for k, v in libs.items(): if k in f.__name__: serializer = v break def wrapper(self: Dataclass, *args, **kwargs): self._serializer = serializer return f(self, *args, **kwargs) return wrapper @property def to_dict(self) -> dict: """Return a dictionary with all the attributes of the object. Returns: dict """ def iterable_type(var: any) -> type: if isinstance(var, (list, tuple, set)): return var.__class__ return None d = {} for k, v in self.__clean_dict__.items(): if t := iterable_type(v): # handle recursive lists d[k] = t(i.to_dict if isinstance(i, Dataclass) else i for i in v) elif isinstance(v, Dataclass): # handle recursive dataclasses d[k] = v.to_dict else: # simple types d[k] = v return d @property def frozen(self) -> bool: """Return the frozen status of the object.""" return self._frozen @property @_importDecorator def to_json(self) -> str: """ Return a json representation of the object. Attributes are recursively converted to json. Returns: str """ dict_data = self.to_dict # all the sets and tuples are converted to lists # because json doesn't support them for k, v in dict_data.items(): if isinstance(v, (set, tuple)): dict_data[k] = list(v) return self._serializer.dumps(dict_data) @property def to_json_pretty(self) -> str: """Return a pretty json representation of the object. Returns: str """ return self._serializer.dumps(self._serializer.loads(self.to_json), indent=4) @property @_importDecorator def to_toml(self) -> str: """Return a toml representation of the object. Returns: str """ return self._serializer.dumps(self.to_dict) @property @_importDecorator def to_yaml(self) -> str: """Return a yaml representation of the object. Returns: str """ return self._serializer.dump(self.to_dict) @property def attributes(self) -> list: """Return a list of all the attributes of the class. Returns: list """ return list(self.__class_attributes__.keys()) @classmethod @_importDecorator def from_json(cls, json_string: str) -> Dataclass: """Create an object from a json string. Args: json_string (str): json string Returns: Dataclass """ cls._deserialized = True return cls(**cls._serializer.loads(json_string)) @classmethod @_importDecorator def from_toml(cls, toml_string: str) -> Dataclass: """Create an object from a toml string. Args: toml_string (str): toml string Returns: Dataclass """ cls._deserialized = True return cls(**cls._serializer.loads(toml_string)) @classmethod @_importDecorator def from_yaml(cls, yaml_string: str) -> Dataclass: """Create an object from a yaml string. Args: yaml_string (str): yaml string Returns: Dataclass """ cls._deserialized = True return cls( **cls._serializer.load(yaml_string, Loader=cls._serializer.FullLoader) ) @classmethod def from_dict(cls, d: dict) -> Dataclass: """Create an object from a dictionary. Args: d (dict): dictionary Returns: Dataclass """ cls._deserialized = True return cls(**d)
Static methods
def from_dict(d: dict) ‑> Dataclass
-
Create an object from a dictionary.
Args
d
:dict
- dictionary
Returns
Dataclass
Expand source code
@classmethod def from_dict(cls, d: dict) -> Dataclass: """Create an object from a dictionary. Args: d (dict): dictionary Returns: Dataclass """ cls._deserialized = True return cls(**d)
def from_json(*args, **kwargs)
-
Expand source code
def wrapper(self: Dataclass, *args, **kwargs): self._serializer = serializer return f(self, *args, **kwargs)
def from_toml(*args, **kwargs)
-
Expand source code
def wrapper(self: Dataclass, *args, **kwargs): self._serializer = serializer return f(self, *args, **kwargs)
def from_yaml(*args, **kwargs)
-
Expand source code
def wrapper(self: Dataclass, *args, **kwargs): self._serializer = serializer return f(self, *args, **kwargs)
Instance variables
var attributes : list
-
Return a list of all the attributes of the class.
Returns
list
Expand source code
@property def attributes(self) -> list: """Return a list of all the attributes of the class. Returns: list """ return list(self.__class_attributes__.keys())
var frozen : bool
-
Return the frozen status of the object.
Expand source code
@property def frozen(self) -> bool: """Return the frozen status of the object.""" return self._frozen
var to_dict : dict
-
Return a dictionary with all the attributes of the object.
Returns
dict
Expand source code
@property def to_dict(self) -> dict: """Return a dictionary with all the attributes of the object. Returns: dict """ def iterable_type(var: any) -> type: if isinstance(var, (list, tuple, set)): return var.__class__ return None d = {} for k, v in self.__clean_dict__.items(): if t := iterable_type(v): # handle recursive lists d[k] = t(i.to_dict if isinstance(i, Dataclass) else i for i in v) elif isinstance(v, Dataclass): # handle recursive dataclasses d[k] = v.to_dict else: # simple types d[k] = v return d
var to_json
-
Expand source code
def wrapper(self: Dataclass, *args, **kwargs): self._serializer = serializer return f(self, *args, **kwargs)
var to_json_pretty : str
-
Return a pretty json representation of the object.
Returns
str
Expand source code
@property def to_json_pretty(self) -> str: """Return a pretty json representation of the object. Returns: str """ return self._serializer.dumps(self._serializer.loads(self.to_json), indent=4)
var to_toml
-
Expand source code
def wrapper(self: Dataclass, *args, **kwargs): self._serializer = serializer return f(self, *args, **kwargs)
var to_yaml
-
Expand source code
def wrapper(self: Dataclass, *args, **kwargs): self._serializer = serializer return f(self, *args, **kwargs)
Methods
def freeze(self) ‑> None
-
Freeze the class.
After this, attributes cannot be changed. The action cannot be undone.
Expand source code
def freeze( self, ) -> None: """Freeze the class. After this, attributes cannot be changed. The action cannot be undone. """ self._frozen = True