Source code for dantro.mixins.indexing

"""This module implement mixin classes that provide indexing capabilities"""

import logging
from typing import Union

from .base import ItemAccessMixin
from ..abc import AbstractDataContainer, AbstractDataGroup

# Local constants
log = logging.getLogger(__name__)

# -----------------------------------------------------------------------------

[docs]class IntegerItemAccessMixin: """This mixin allows accessing items via integer keys and also supports calling the __contains__ magic method with integer keys. It is meant to be used to add features to an AbstractDataGroup-derived class, although this is not enforced. NOTE The __setitem__ method is not covered by this! NOTE The class using this mixin has to implement index access methods and the __contains__ magic method independently from this mixin! """
[docs] def _parse_key(self, key: Union[str, int]) -> str: """Makes sure a key is a string""" if isinstance(key, int): return str(key) return key
[docs] def __getitem__(self, key: Union[str, int]): """Adjusts the parent method to allow integer key item access""" return super().__getitem__(self._parse_key(key))
# NOTE Don't need two-argument super() here because the parents are # deduced when the class is assembled, i.e. when mixins and other # inheritances are all combined.
[docs] def __delitem__(self, key: Union[str, int]): """Adjusts the parent method to allow item deletion by integer key""" return super().__delitem__(self._parse_key(key))
[docs] def __contains__(self, key: Union[str, int]) -> bool: """Adjusts the parent method to allow checking for integers""" return super().__contains__(self._parse_key(key))
[docs]class PaddedIntegerItemAccessMixin(IntegerItemAccessMixin): """This mixin allows accessing items via integer keys that map to members that have a zero-padded integer name. It can only be used as mixin for AbstractDataGroup-derived classes! The __contains__ magic method is also supported in this mixin. NOTE The class using this mixin has to implement index access methods and the __contains__ magic method independently from this mixin! """ # The number of digits of the padded string representing the integer _PADDED_INT_KEY_WIDTH = None # The format string to generate a padded integer; deduced upon first call _PADDED_INT_FSTR = None # Whether to use strict checking when parsing keys, i.e.: check that the # range of keys is valid and an error is thrown when an integer key was # given that cannot be represented consistently by a padded string of the # determined key width. _PADDED_INT_STRICT_CHECKING = True # The allowed maximum value of an integer key; checked only in strict mode _PADDED_INT_MAX_VAL = None # ......................................................................... @property def padded_int_key_width(self) -> Union[int, None]: """Returns the width of the zero-padded integer key or None, if it is not already specified. """ return self._PADDED_INT_KEY_WIDTH @padded_int_key_width.setter def padded_int_key_width(self, key_width: int): """Method to manually provide an integer key width Args: key_width (int): The Raises: ValueError: Description """ if self._PADDED_INT_FSTR: raise ValueError("Padded integer key width is already set for {}; " "cannot set it again!".format(self.logstr)) elif key_width <= 0: raise ValueError("Argument `key_width` to padded_int_key_width " "setter property of {} needs to be positive, was " "'{}'!".format(self.logstr, key_width)) # Deduce the key width by going over all member names self._PADDED_INT_KEY_WIDTH = key_width # Assemble the format string to something like {:05d} self._PADDED_INT_FSTR = "{:0" + str(self._PADDED_INT_KEY_WIDTH) + "d}" # Compute the maximum value that is fully representable by the string self._PADDED_INT_MAX_VAL = 10**self._PADDED_INT_KEY_WIDTH - 1 # .........................................................................
[docs] def _parse_key(self, key: Union[str, int]) -> str: """Parse a potentially integer key to a zero-padded string""" # Check if it even is an integer if not isinstance(key, int): return key # Also, cannot work properly if no format string was generated, i.e. no # key width was provided. This can be the case if there are no members # added to the group using this mixin yet if not self._PADDED_INT_FSTR: return str(key) # Optionally, make an additional check for integer key values if self._PADDED_INT_STRICT_CHECKING: if key < 0 or key > self._PADDED_INT_MAX_VAL: raise IndexError("Integer index {} out of range [0, {}] for " "{}!".format(key, self._PADDED_INT_MAX_VAL, self.logstr)) # Generate the key string from the format string return self._PADDED_INT_FSTR.format(key)
[docs] def _check_cont(self, cont: AbstractDataContainer) -> None: """This method is invoked when adding a member to a group and makes sure the name of the added group is correctly zero-padded. Also, upon first call, communicates the zero padded integer key width, i.e.: the length of the container name, to the PaddedIntegerItemAccessMixin. Args: cont: The member container to add Returns None: No return value needed """ if self.padded_int_key_width is None: self.padded_int_key_width = len(cont.name) if len(cont.name) != self.padded_int_key_width: raise ValueError("All containers that are to be added to {} need " "names of the same length ({}), but {} had a " "name of length {}!" "".format(self.logstr, self.padded_int_key_width, cont.logstr, len(cont.name))) # Still invoke the potentially existing parent method super()._check_cont(cont)