Source code for dantro.mixins.base

"""This sub-module implements the basic mixin classes that are required
in the dantro.base module"""

import sys
import logging
import warnings

from ..abc import AbstractDataProxy, PATH_JOIN_CHAR
from ..tools import TTY_COLS

# Local constants
log = logging.getLogger(__name__)


[docs]class UnexpectedTypeWarning(UserWarning): """Given when there was an unexpected type passed to a data container.""" pass
# -----------------------------------------------------------------------------
[docs]class AttrsMixin: """This Mixin class supplies the `attrs` property getter and setter and the private `_attrs` attribute. Hereby, the setter function will initialize a BaseDataAttrs-derived object and store it as an attribute. This relays the checking of the correct attribute format to the actual BaseDataAttrs-derived class. For changing the class that is used for the attributes, an overwrite of the _ATTRS_CLS class variable suffices. """ # The class attribute that the attributes will be stored to _attrs = None # Define the class to use for storing attributes _ATTRS_CLS = None @property def attrs(self): """The container attributes.""" return self._attrs @attrs.setter def attrs(self, new_attrs): """Setter method for the container `attrs` attribute.""" if self._ATTRS_CLS is None: raise ValueError("Need to declare the class variable _ATTRS_CLS " "in order to use the AttrsMixin!") # Perform the initialisation self._attrs = self._ATTRS_CLS(name='attrs', attrs=new_attrs)
[docs]class SizeOfMixin: """Provides the __sizeof__ magic method and attempts to take into account the size of the attributes. """
[docs] def __sizeof__(self) -> int: """Returns the size of the data (in bytes) stored in this container's data and its attributes. Note that this value is approximate. It is computed by calling the ``sys.getsizeof`` function on the data, the attributes, the name and some caching attributes that each dantro data tree class contains. Importantly, this is not a recursive algorithm. Also, derived classes might implement further attributes that are not taken into account either. To be more precise in a subclass, create a specific __sizeof__ method and invoke this parent method additionally. For more information, see the documentation of ``sys.getsizeof``: https://docs.python.org/3/library/sys.html#sys.getsizeof """ nbytes = sys.getsizeof(self._data) nbytes += sys.getsizeof(self._attrs) nbytes += sys.getsizeof(self._name) nbytes += sys.getsizeof(self._logstr) return nbytes
[docs]class LockDataMixin: """This Mixin class provides a flag for marking the data of a group or container as locked. """ # Whether the data is regarded as locked. Note name-mangling here. __locked = False @property def locked(self) -> bool: """Whether this object is locked""" return self.__locked
[docs] def lock(self): """Locks the data of this object""" self.__locked = True self._lock_hook()
[docs] def unlock(self): """Unlocks the data of this object""" self.__locked = False self._unlock_hook()
[docs] def raise_if_locked(self, *, prefix: str=None): """Raises an exception if this object is locked; does nothing otherwise """ if self.locked: raise RuntimeError("{}Cannot modify {} because it was already " "marked locked." "".format(prefix + " " if prefix else "", self.logstr))
[docs] def _lock_hook(self): """Invoked upon locking.""" pass
[docs] def _unlock_hook(self): """Invoked upon unlocking.""" pass
[docs]class CollectionMixin: """This Mixin class implements the methods needed for being a Collection. It relays all calls forward to the data attribute. """
[docs] def __contains__(self, key) -> bool: """Whether the given key is contained in the items.""" return bool(key in self.data)
[docs] def __len__(self) -> int: """The number of items.""" return len(self.data)
[docs] def __iter__(self): """Iterates over the items.""" return iter(self.data)
[docs]class ItemAccessMixin: """This Mixin class implements the methods needed for getting, setting, and deleting items. It relays all calls forward to the data attribute, but if given a list (passed down from above), it extracts it """
[docs] def __getitem__(self, key): """Returns an item.""" return self.data[self._item_access_convert_list_key(key)]
[docs] def __setitem__(self, key, val): """Sets an item.""" self.data[self._item_access_convert_list_key(key)] = val
[docs] def __delitem__(self, key): """Deletes an item""" del self.data[self._item_access_convert_list_key(key)]
[docs] def _item_access_convert_list_key(self, key): """If given something that is not a list, just return that key""" if isinstance(key, list): if len(key) > 1: return tuple(key) return key[0] return key
[docs]class MappingAccessMixin(ItemAccessMixin, CollectionMixin): """Supplies all methods that are needed for Mapping access. All calls are relayed to the data attribute. """
[docs] def keys(self): """Returns an iterator over the attribute names.""" return self.data.keys()
[docs] def values(self): """Returns an iterator over the attribute values.""" return self.data.values()
[docs] def items(self): """Returns an iterator over the (keys, values) tuple of the attributes.""" return self.data.items()
[docs] def get(self, key, default=None): """Return the value at `key`, or `default` if `key` is not available.""" return self.data.get(key, default)
[docs]class CheckDataMixin: """This mixin class extends a BaseDataContainer-derived class to check the provided data before storing it in the container. It implements a general _check_data method, overwriting the placeholder method in the BaseDataContainer, and can be controlled via class variables. .. note:: This is not suitable for checking containers that are added to an object of a BaseDataGroup-derived class! Attributes: DATA_ALLOW_PROXY (bool): Whether to allow _all_ proxy types, i.e. classes derived from AbstractDataProxy DATA_EXPECTED_TYPES (tuple, None): Which types to allow. If None, all types are allowed. DATA_UNEXPECTED_ACTION (str): The action to take when an unexpected type was supplied. Can be: raise, warn, ignore """ # Specify expected data types for this container class DATA_EXPECTED_TYPES = None # as tuple or None (allow all) DATA_ALLOW_PROXY = False # to check for AbstractDataProxy DATA_UNEXPECTED_ACTION = 'warn' # Can be: raise, warn, ignore
[docs] def _check_data(self, data) -> None: """A general method to check the received data for its type Args: data: The data to check Raises: TypeError: If the type was unexpected and the action was 'raise' ValueError: Illegal value for DATA_UNEXPECTED_ACTION class variable Returns: None """ if self.DATA_EXPECTED_TYPES is None: # All types allowed return # Compile tuple of allowed types expected_types = self.DATA_EXPECTED_TYPES if self.DATA_ALLOW_PROXY: expected_types += (AbstractDataProxy,) # Check for expected types if isinstance(data, expected_types): return # else: was not of the expected type # Create a base message msg = ("Unexpected type {} for data passed to {}! " "Expected types are: {}.".format(type(data), self.logstr, expected_types)) # Handle according to the specified action if self.DATA_UNEXPECTED_ACTION == 'raise': raise TypeError(msg) elif self.DATA_UNEXPECTED_ACTION == 'warn': warnings.warn(msg + "\nInitialization will work, but be informed " "that there might be errors at runtime.", UnexpectedTypeWarning) elif self.DATA_UNEXPECTED_ACTION == 'ignore': log.debug(msg + " Ignoring ...") else: raise ValueError("Illegal value '{}' for class variable " "DATA_UNEXPECTED_ACTION of {}. " "Allowed values are: raise, warn, ignore" "".format(self.DATA_UNEXPECTED_ACTION, self.classname))