"""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)