As unfortunate as this is, I don't think there's an easy way to solve this while adhering to descriptors and the attribute lookup model. This is a consequence of the following rule:
> object.__getattr__(self, name):
> Called when the default attribute access fails with an
> AttributeError (either __getattribute__() raises an AttributeError
> because name is not an instance attribute or an attribute in the
> class tree for self; or __get__() of a name property raises
> AttributeError)
As it notes, if the __get__() raises an AttributeError then a fallback to __getattr__ is initiated. One may think that maybe we can just catch AttributeError in @property to try to fix this problem but a quick search shows that people do intentionally raise AttributeError in @property methods:
* https://github.com/kdschlosser/EventGhost-TPLink/blob/a4a642fde8dd4deba66262a36d673cbbf71b8ceb/TPLink/tp_link/rule.py#L148-L152
* https://github.com/ajayau404/sniffer/blob/cd0c813b8b526a3c791735a41b13c7677eb4aa0e/lib/python3.5/site-packages/vpython/vpython.py#L1942-L1944
While this in combination with a __getattr__ is rare, I was able to find one example:
* https://github.com/xrg/behave_manners/blob/19a5feb0b67fe73cd902a959f0d038b905a69b38/behave_manners/context.py#L37
I don't think that changing this behavior is acceptable as people might be relying on it and it's well documented.
In your case, I think it's really the "catch-all" __getattr__ that's at fault here which really shouldn't be returning None for all attributes blindly. This does bring up a good point though that in the process of this fall-back behavior, the original AttributeError from A's property does get masked. What can be done and might be a good idea is to show the original AttributeError failure as the cause for the second. Something like this:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "<stdin>", line 5, in myprop
AttributeError: 'int' object has no attribute 'foo'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 7, in <module>
File "<stdin>", line 5, in <module>
File "<stdin>", line 7, in __getattr__
AttributeError: not found in __getattr__
This at least somewhat indicates that the original descriptor __get__ failed and then __getattr__ also raised. As opposed to now where the original exception gets masked:
>>> class A:
... @property
... def myprop(self):
... a = 1
... a.foo
... def __getattr__(self, attr_name):
... raise AttributeError("not found in __getattr__")
...
>>> a = A()
>>> a.myprop
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __getattr__
AttributeError: not found in __getattr__
I'm gonna take a stab at implementing this real quick to see if it's actually useful and viable.
|