nicholasmireles / DotDict

A simple Python library to make chained attributes possible.
GNU General Public License v3.0
231 stars 3 forks source link

Broken in Python 3.11 #2

Open Chaz6 opened 1 year ago

Chaz6 commented 1 year ago
$ pip3 install -U --user attr-dot-dict
Collecting attr-dot-dict
  Obtaining dependency information for attr-dot-dict from https://files.pythonhosted.org/packages/af/ab/3c717181e61cfa97b71e6e01d072725c2d323350b408ab43547d02bc19ad/attr_dot_dict-0.1.0-py3-none-any.whl.metadata
  Downloading attr_dot_dict-0.1.0-py3-none-any.whl.metadata (42 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 42.0/42.0 kB 1.6 MB/s eta 0:00:00
Downloading attr_dot_dict-0.1.0-py3-none-any.whl (27 kB)
Installing collected packages: attr-dot-dict
Successfully installed attr-dot-dict-0.1.0
$ python3
Python 3.11.4 (main, Jun  8 2023, 19:12:32) [Clang 15.0.7 (Red Hat 15.0.7-2.el9)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from dotdict import DotDict as dd
>>> a = dd()
>>> a.b.c.d = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/chaz/.local/lib/python3.11/site-packages/dotdict.py", line 24, in __getattr__
    raise AttributeError("No attribute '%s'" % __name)
AttributeError: No attribute 'c'
>>>
nicholasmireles commented 1 year ago

I'm curious to see what exactly changed in 3.11, because after delving deeper into the interpreter and such, I see a lot of "changed in 3.11".

matt-manes commented 7 months ago

I don't think it was 3.11, but the changes introduced in commit 9893e1a. Commits prior to that one work as expected running 3.11. 9893e1a fails with a recursion error and the current commit (eed0e84) fails with the attribute error above. Technically the current commit works up to the depth of c (a.b.c = 1), but fails at anything deeper than that like a.b.c.d = 1. It appears the not self._temp condition in the if statement of DotDict.__getattr__ is the culprit:

def __getattr__(self, __name: str) -> Any:
    if __name not in self.__dict__ and not self._temp:
        self.__dict__[__name] = DotDict(temp=True, key=__name, parent=self)
    else:
        del self._parent.__dict__[self._key]
        raise AttributeError("No attribute '%s'" % __name)
    return self.__dict__[__name]

When doing a.b.c.d = 1, b gets added to a, but with temp=True. So then when it comes to c, since self._temp is True for b, the else statement executes and the error is raised.