class delegatee:
"""Delegatee class, used in place of an iterable when defining delegates dictionary.
This class provides the following features:
- It allows to validate the delegatee class attributes/methods.
- It supports the "*" argument in attrs parameter, which automatically detects all non-dunder methods of the
delegatee class.
- It enables adding prefix and/or suffix to non-dunder methods.
Arguments:
delegatee_cls: Class from which we delegate. This is the class/type definition, no need to instantiate it.
attrs: Sequence of attributes/methods to inject on the composed class.
!!! warning
If `"*"` is present, we inject all the methods, **excluding** dunder methods, which need to be
explicitly stated.
prefix: Injected methods prefix, unused for dunder methods.
suffix: Injected methods suffix, unused for dunder methods.
validate: Whether or not to validate if `delegatee_cls` has all the methods and/or attributes.
!!! warning
- Methods are searched in class definition `__dict__`.
- Attributes are searched in class `__init__` code by matching the following regex:
`"self.{attr}"` (more technically, `re.compile(r"self.(\w+)")`).
Methods:
- _parse_attrs: Parses the original attrs sequence, splitting between dunder and class methods.
- _is_dunder_method: Assess whether or not an attribute is a dunder method.
- _validate_delegatee_methods: Checks if delegatee_cls has all attributes/methods in attrs.
"""
def __init__(
self,
delegatee_cls: Type,
attrs: Iterable[str],
prefix: str = "",
suffix: str = "",
validate: bool = True,
):
if not attrs: # empty iterable such as list(), tuple(), None, etc...
raise ValueError("attrs parameter cannot be None")
self.delegatee_cls = delegatee_cls
if delegatee_cls is not None:
self._attrs = self._parse_attrs(delegatee_cls, attrs)
else:
self._attrs = tuple(attrs)
if validate and (delegatee_cls is not None):
self._validate_delegatee_methods(self.delegatee_cls, self._attrs)
self._prefix = prefix
self._suffix = suffix
def __iter__(self):
for attr_name in self._attrs:
yield attr_name
@staticmethod
def _parse_attrs(delegatee_cls: Type, attrs: Iterable[str]) -> Tuple[str, ...]:
"""Parses the original attrs sequence:
- Splits between dunder and class methods.
- If `"*"` is present, we add all the methods to the list of methods to inject, excluding dunder methods which
need to be explicitly stated.
- If `"*"` is not present, we simply return the original attrs sequence.
"""
dunder_methods, base_methods = partition(delegatee._is_dunder_method, attrs)
if "*" in base_methods:
pattern = re.compile(r"self.(\w+)")
methods = tuple(
attr_name for attr_name in delegatee_cls.__dict__.keys() if not delegatee._is_dunder_method(attr_name)
)
try:
co_code = inspect.getsource(delegatee_cls.__init__)
init_attrs = tuple(pattern.findall(co_code))
except Exception as e:
logger.info(f"Unable to parse __init__ method of {delegatee_cls} due to: {e}")
init_attrs = tuple()
all_methods = methods + init_attrs
else:
all_methods = base_methods
return dunder_methods + all_methods
@staticmethod
def _is_dunder_method(attr_name: str) -> bool:
"""Assesses whether or not `attr_name` is a dunder method by checking if it startsand ends with "__"."""
return attr_name.startswith("__") and attr_name.endswith("__")
@staticmethod
def _validate_delegatee_methods(delegatee_cls: Type, attrs: Iterable[str]) -> None:
"""Checks if `delegatee_cls` has all attributes and methods listed in attrs.
Arguments:
delegatee_cls: Class from which we delegate. This is the class definition, no need to instantiate it.
attrs: Sequence of attributes/methods to inject on the composed class.
Raises:
AttributeError: if `delegatee_cls` has no attribute/method in attrs.
"""
cls_methods = tuple([a[0] for a in inspect.getmembers(delegatee_cls)])
try:
co_code = inspect.getsource(delegatee_cls.__init__)
pattern = re.compile(r"self.(\w+)")
init_attrs = tuple(pattern.findall(co_code))
except Exception as e:
logger.info(f"Unable to parse `__init__` method of {delegatee_cls} due to error: {e}")
init_attrs = tuple()
all_methods = cls_methods + init_attrs
for attr_name in attrs:
if attr_name not in all_methods:
raise AttributeError(f"'{delegatee_cls}' has no attribute nor method '{attr_name}'")