from abc import ABC
from collections import OrderedDict
from inspect import Parameter, Signature
from functools import wraps
from warnings import warn
from addict import Dict
import numpy as np
# %% Descriptors
[docs]
class Descriptor(ABC):
"""Base class for descriptors.
Descriptors are used to efficiently implement class attributes that
require checking type, value, size etc.
For using Descriptors, a class must inherit from StructMeta.
- ``self`` is the Descriptor managing the attribute of the ``instance``.
- ``instance`` is the object which holds the actual ``value``.
- ``value`` is the value of the ``instance`` attribute.
See Also
--------
StructMeta
Parameters
"""
def __init__(self, *args, **kwargs):
pass
def __get__(self, instance, cls):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if value is None:
try:
del instance.__dict__[self.name]
except KeyError:
pass
return
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
[docs]
class Aggregator():
"""Descriptor aggregating parameters from instance container with other instances."""
def __init__(self, parameter_name, container, *args, **kwargs):
"""
Initialize the Aggregator descriptor.
Parameters
----------
parameter_name : str
Name of the parameter to be aggregated.
container : str
Name of the attribute in the instance that contains the other instances
from which parameters will be aggregated.
*args : tuple, optional
Additional positional arguments.
**kwargs : dict, optional
Additional keyword arguments.
"""
self.parameter_name = parameter_name
self.container = container
def _container_obj(self, instance):
"""
Retrieve the iterable container of the instance.
Parameters
----------
instance : Any
Instance to retrieve the container from.
Returns
-------
obj : iterable
Iterable container of the instance.
Raises
------
TypeError
If the container is not iterable.
"""
container = getattr(instance, self.container)
if not hasattr(container, "__iter__"):
raise TypeError(f"{self.container} attribute is not iterable")
return container
def _n_instances(self, instance):
return len(self._get_parameter_values_from_container(instance))
def _get_parameter_values_from_container(self, instance):
container = self._container_obj(instance)
value = [getattr(el, self.parameter_name) for el in container]
if len(value) == 0:
return
return value
def __get__(self, instance, cls):
"""
Retrieve the descriptor value for the given instance.
Parameters
----------
instance : Any
Instance to retrieve the descriptor value for.
cls : Type[Any], optional
Class to which the descriptor belongs. By default None.
Returns
-------
np.array
Descriptor values aggregated in a numpy array.
"""
if instance is None:
return self
value = self._get_parameter_values_from_container(instance)
if value is not None:
self._check(instance, value, recursive=True)
return value
def __set__(self, instance, value):
"""
Set the descriptor value for the given instance.
Parameters
----------
instance : Any
Instance to set the descriptor value for.
value : Any
Value to set.
"""
if value is not None:
value = self._prepare(instance, value, recursive=True)
self._check(instance, value, recursive=True)
container = self._container_obj(instance)
for i, el in enumerate(container):
setattr(el, self.parameter_name, value[i])
def _prepare(self, instance, value, recursive=False):
"""
Prepare value for setting if necessary.
Override this method if type-casting or other operations are necessary.
Parameters
----------
instance : Any
Instance to retrieve the descriptor value for.
value : Any
Value to cast.
Returns
-------
Any
Prepared value.
"""
return value
def _check(self, instance, value, recursive=False):
"""
Check the given value.
Override this method for specific checks.
Parameters
----------
instance : Any
Instance to retrieve the descriptor value for.
value : Any
Value to check.
recursive : bool, optional
If True, perform the check recursively. Defaults to False.
"""
return
[docs]
def make_signature(names):
return Signature(
Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
for name in names)
# %% Stucture / ParameterHandler
[docs]
class Structure(metaclass=StructMeta):
"""
A class representing a structured data entity.
This class is designed to work in conjunction with the `StructMeta` metaclass
to handle descriptors and related parameters.
Attributes
----------
_parameters : dict
Dictionary of parameters associated with the instance.
_sized_parameters : list
List of parameters that have a `size` attribute.
_polynomial_parameters : list
List of parameters with `fill_values` attribute.
_required_parameters : list
List of parameters that have a default value of None.
"""
def __init__(self, *args, **kwargs):
"""
Initialize a Structure instance.
Parameters are bound to the instance based on the `__signature__`
defined by the metaclass.
Parameters
----------
*args
Positional arguments representing parameters.
**kwargs
Keyword arguments representing parameters.
"""
self._parameters_dict = Dict()
for param in self._parameters:
value = getattr(self, param)
if param in self._optional_parameters and value is None:
continue
self._parameters_dict[param] = value
bound = self.__signature__.bind_partial(*args, **kwargs)
for name, val in bound.arguments.items():
setattr(self, name, val)
@property
def parameters(self):
"""dict: Parameters of the instance."""
parameters = self._parameters_dict
parameters.update(self.aggregated_parameters)
return parameters
@parameters.setter
def parameters(self, parameters):
"""Set parameters for the instance.
Parameters
----------
parameters : dict
A dictionary of parameters to set.
Raises
------
ValueError
If any of the provided parameters is not valid.
"""
for param, value in parameters.items():
if param not in self._parameters:
raise ValueError('Not a valid parameter.')
if value is not None:
setattr(self, param, value)
self._parameters_dict[param] = value
@property
def sized_parameters(self):
"""dict: Sized parameters of the instance."""
parameters = {
key: value for key, value in self.parameters.items()
if key in self._sized_parameters
}
return Dict(parameters)
@property
def aggregated_parameters(self):
"""dict: Aggregated parameters of the instance."""
parameters = {
key: getattr(self, key) for key in self._aggregators
}
return Dict(parameters)
@property
def polynomial_parameters(self):
"""dict: Polynomial parameters of the instance."""
parameters = {
key: value for key, value in self.parameters.items()
if key in self._polynomial_parameters
}
return Dict(parameters)
@property
def required_parameters(self):
"""list: Parameters that have no default value."""
return self._required_parameters
@property
def missing_parameters(self):
"""list: Parameters that are required but not set."""
missing_parameters = []
for param in self.required_parameters:
if getattr(self, param) is None:
missing_parameters.append(param)
return missing_parameters
[docs]
def check_required_parameters(self):
"""
Verify if all required parameters are set.
Returns
-------
bool
True if all required parameters are set. False otherwise.
Raises
------
Warning
If any of the required parameters are missing.
"""
if len(self.missing_parameters) == 0:
return True
else:
for param in self.missing_parameters:
warn(f'Missing parameter "{param}".')
return False
[docs]
def frozen_attributes(cls):
"""Decorate classes to prevent setting attributes after the init method."""
cls._is_frozen = False
def frozensetattr(self, key, value):
if self._is_frozen and not hasattr(self, key):
raise AttributeError(
f"{cls.__name__} object has no attribute {key}"
)
else:
object.__setattr__(self, key, value)
def init_decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
func(self, *args, **kwargs)
self._is_frozen = True
return wrapper
cls.__setattr__ = frozensetattr
cls.__init__ = init_decorator(cls.__init__)
return cls