Infinite Recursion When Using store_attr and Overwriting __getattr__

Python
Fastai
An infinite recursion when overwriting __getattr__ while store_attr is used.
Author

Ziyue Li

Published

November 16, 2022

from fastcore.basics import store_attr

class Table:
    def __init__(self, name) -> None:
        store_attr()

    def __getattr__(self, __name: str):
        return f"{self.name}.{__name}"


t = Table('tbl')
t.column
RecursionError: maximum recursion depth exceeded while calling a Python object

If we take a look at store_attr, we see that the problem occurs at the step where store_attr() calls hasattr(self, '__slots__'), which calls __getattr__ when __slots__ is not available. In fact, store_attr calls a few attributes that start with __.

def store_attr(names=None, self=None, but='', cast=False, store_args=None, **attrs):
    "Store params named in comma-separated `names` from calling context into attrs in `self`"
    fr = sys._getframe(1)
    args = argnames(fr, True)
    if self: args = ('self', *args)
    else: self = fr.f_locals[args[0]]
    if store_args is None: store_args = not hasattr(self,'__slots__')
    if store_args and not hasattr(self, '__stored_args__'): self.__stored_args__ = {}
    anno = annotations(self) if cast else {}
    if names and isinstance(names,str): names = re.split(', *', names)
    ns = names if names is not None else getattr(self, '__slots__', args[1:])
    added = {n:fr.f_locals[n] for n in ns}
    attrs = {**attrs, **added}
    if isinstance(but,str): but = re.split(', *', but)
    attrs = {k:v for k,v in attrs.items() if k not in but}
    return _store_attr(self, anno, **attrs)

Therefore, if we want to use store_attr(), when overwriting __getattr__, we need to protect those called by store_attr(), otherwise there will be an infinite loop.

from fastcore.basics import store_attr

class Table:
    def __init__(self, name) -> None:
        store_attr()

    def __getattr__(self, __name: str):
        if __name.startswith('__'):
            return super().__getattr__(__name)
        else:
            return f"{self.name}.{__name}"


t = Table('tbl')
t.column
'tbl.column'