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                            Traceback (most recent call last)
/var/folders/vv/syvp2m7117s_1y8m9bwlj28m0000gq/T/ipykernel_45310/3176737333.py in <module>
      9 
     10 
---> 11 t = Table('tbl')
     12 t.column

/var/folders/vv/syvp2m7117s_1y8m9bwlj28m0000gq/T/ipykernel_45310/3176737333.py in __init__(self, name)
      3 class Table:
      4     def __init__(self, name) -> None:
----> 5         store_attr()
      6 
      7     def __getattr__(self, __name: str):

/Applications/Anaconda3/envs/py37/lib/python3.7/site-packages/fastcore/basics.py in store_attr(names, self, but, cast, store_args, **attrs)
    397     if self: args = ('self', *args)
    398     else: self = fr.f_locals[args[0]]
--> 399     if store_args is None: store_args = not hasattr(self,'__slots__')
    400     if store_args and not hasattr(self, '__stored_args__'): self.__stored_args__ = {}
    401     anno = annotations(self) if cast else {}

/var/folders/vv/syvp2m7117s_1y8m9bwlj28m0000gq/T/ipykernel_45310/3176737333.py in __getattr__(self, _Table__name)
      6 
      7     def __getattr__(self, __name: str):
----> 8         return f"{self.name}.{__name}"
      9 
     10 

... last 1 frames repeated, from the frame below ...

/var/folders/vv/syvp2m7117s_1y8m9bwlj28m0000gq/T/ipykernel_45310/3176737333.py in __getattr__(self, _Table__name)
      6 
      7     def __getattr__(self, __name: str):
----> 8         return f"{self.name}.{__name}"
      9 
     10 

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'