Source code for dantro.mixins.indexing

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

import logging
from typing import Union

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

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 __setitem__(self, key: Union[str, int]): """Adjusts the parent method to allow item setting by integer key""" return super().__setitem__(self._parse_key(key))
[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 :py:class:`~dantro.abc.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! """ _PADDED_INT_KEY_WIDTH: int = None """The number of digits of the padded string representing the integer""" _PADDED_INT_FSTR: str = None """The format string to generate a padded integer; deduced upon first call """ _PADDED_INT_STRICT_CHECKING: bool = True """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_MAX_VAL: int = None """The allowed maximum value of an integer key; checked only in strict mode """ # ......................................................................... @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( f"Padded integer key width is already set for {self.logstr}; " "cannot set it again!" ) elif key_width <= 0: raise ValueError( "Argument `key_width` to padded_int_key_width setter property " f"of {self.logstr} needs to be positive, was '{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( f"All containers that are to be added to {self.logstr} need " f"names of the same length ({self.padded_int_key_width}), " f"but {cont.logstr} had a name of length {len(cont.name)}!" ) # Still invoke the potentially existing parent method super()._check_cont(cont)