classification
Title: descriptor protocol bug
Type: behavior Stage:
Components: Interpreter Core Versions: Python 3.4, Python 3.3, Python 3.2, Python 2.7
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: BreamoreBoy, Micah.Friesen, benjamin.peterson, daniel.urban, eric.araujo, eric.snow, gangesmaster, pitrou, rhettinger, thomaslee
Priority: high Keywords: patch

Created on 2007-12-13 20:13 by gangesmaster, last changed 2013-02-23 06:17 by eric.snow.

Files
File name Uploaded Description Edit
demo.txt gangesmaster, 2007-12-13 20:13
getattr-descriptors.diff eric.snow, 2013-02-23 06:17 review
Messages (10)
msg58581 - (view) Author: ganges master (gangesmaster) Date: 2007-12-13 20:13
it seems the code of PyObject_GenericGetAttr, which invokes the
descriptor protocol, silences any AttributeErrors raised by the
descriptor, for classes that also define __getattr__. it should
propagate up rather than being silently ignored.

the attached example is quite artificial, but it's a simplification of
real world code i had hard time debugging. turned out i misspelled an
attribute name inside the property getter function, which raised an
AttributeError as expected -- but the exception i got was quite
misleading, saying the instance has no attribute named so.

this bug only happens when the class defines a custom __getattr__. see
attached demo file for details.
msg61312 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2008-01-20 17:29
I can confirm that with SVN trunk, and it's actually even worse because
it can return unexpected results without raising an exception at all:

>>> class Foo(object):
...   def __getattr__(self, name): return 42
...   @property
...   def bacon(self): return int.lalala
... 
>>> f = Foo()
>>> f.bacon
42
>>> Foo.bacon.__get__(f)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in bacon
AttributeError: type object 'int' has no attribute 'lalala'
msg61321 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2008-01-20 18:09
PyObject_GenericGetAttr is invoked from slot_tp_getattr_hook in
typeobject.c via tp_getattro. The problem is that, when tp_getattro
returns with an AttributeError, there is no way for slot_tp_getattr_hook
to know whether the error was raised by PyObject_GenericGetAttr itself
or by subsequent invocation of user code. Perhaps by adding an attribute
to the raised AttributeError?
msg76825 - (view) Author: ganges master (gangesmaster) Date: 2008-12-03 12:20
here's a short example of the bug

>>> class Foo(object):
...   def __getattr__(self, name): 
...     return 42
...   @property
...   def bacon(self): 
...     return int.lalala
...   @property
...   def eggs(self): 
...     return 17
... 
>>> f = Foo()
>>> f.bacon   # raises an AttributeError, and silently ignores it
42
>>> f.eggs
17
>>> 

are there any news in this front?
msg77424 - (view) Author: Thomas Lee (thomaslee) (Python committer) Date: 2008-12-09 15:20
Related reading from a few years back:

http://coding.derkeiler.com/Archive/Python/comp.lang.python/2005-05/msg03829.html
msg116804 - (view) Author: Mark Lawrence (BreamoreBoy) Date: 2010-09-18 15:53
This is still an issue with the latest trunk.
msg116805 - (view) Author: Benjamin Peterson (benjamin.peterson) * (Python committer) Date: 2010-09-18 15:57
I consider this to be a feature. Properties can raise AttributeError to defer to __getattr__.
msg158556 - (view) Author: Micah Friesen (Micah.Friesen) Date: 2012-04-17 16:16
I ran into this recently, as well, and have lost probably a day's worth of time debugging it. I submit that this is not a feature - I can't imagine a real-world scenario where you actually want to write debuggable code where a descriptor defers to __getattr__ (except perhaps for exception handling, in which case some re-factoring is in order), particularly because descriptors are effectively mix-ins and can be used on multiple classes.

I worked around this by writing an ancestor descriptor that catches AttributeErrors and re-raises them as a user-defined exception.
msg182210 - (view) Author: Eric Snow (eric.snow) * (Python committer) Date: 2013-02-16 04:20
Got bit by a variation of this today in 2.7:


class Spam(object):
    def __getattr__(self, attr):
        raise AttributeError(attr)
    @property
    def eggs(self):
        return self.ham

s = Spam()
s.eggs
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __getattr__
AttributeError: eggs


It took me a little while to figure out what was going on.  A real head-scratcher.  This is because the AttributeError was attributed to the property, but was actually caused by the __getattr__ call triggered by the property's code.  I would expect the AttributeError to reference "ham", not "eggs".  As already noted, if __getattr__() is not there, that's what happens.

Regardless, I'm just not seeing where the hurdle is to improving this behavior.  I certainly agree that this is not a feature.  It is the source of very mysterious failures.

I was surprised that AttributeError does not have an attribute to which the name would be bound.  If it did, then slot_tp_getattr_hook() could check against that:


    if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
        PyObject *tp, *exc, *tb, *exc_attr;

        PyErr_Fetch(&tp, &exc, &tb);
        exc_attr = PyObject_GetAttrString(exc, "attribute");
        PyErr_Restore(tp, exc, tb);

        if (!exc_attr || exc_attr == name) { 
            PyErr_Clear();
            res = call_attribute(self, getattr, name);
        }
        Py_XDECREF(exc_attr);
    }


Alternatively, when an AttributeError comes out of a getter in _PyObject_GenericSetAttrWithDict() (in either spot they're called), another exception (not AttributeError) could be raised with the original chained onto it.  Then slot_tp_getattr_hook() won't silently ignore it.  It would be something like this:


        if (f != NULL && PyDescr_IsData(descr)) {
            res = f(descr, obj, value);
            if (!res && PyErr_ExceptionMatches(PyExc_AttributeError)) {
                PyObject *msg = PyUnicode_FromFormat("getter failed for '%U'", name);
                /* implicit chaining here */
                PyErr_SetObject(PyExc_???Error, msg);
            }
            goto done;
        }


Conceptually, it's as though locating the attribute and extracting the value are lumped together here.  Distinguishing the two would help make this failure situation much less confusing.

Additionally, it would be really helpful to have a brief mention of this behavior (AttributeErrors in getters falling back to __getattr__) in the language reference entry for __getattr__ and/or descriptors.
msg182717 - (view) Author: Eric Snow (eric.snow) * (Python committer) Date: 2013-02-23 06:17
The whole AttributeError gaining an attribute seems impractical, but the second approach still seems reasonable to me.  I've attached a rough patch to demonstrate.  If that looks okay, I can flesh it out.
History
Date User Action Args
2013-02-23 06:17:04eric.snowsetfiles: + getattr-descriptors.diff
keywords: + patch
messages: + msg182717
2013-02-16 04:20:22eric.snowsetnosy: + eric.snow

messages: + msg182210
versions: + Python 3.4
2012-04-18 22:07:08eric.araujosetnosy: + eric.araujo

versions: + Python 3.2, Python 3.3, - Python 2.6
2012-04-17 23:38:11pitrousetnosy: + rhettinger
2012-04-17 16:16:30Micah.Friesensetnosy: + Micah.Friesen
messages: + msg158556
2010-09-18 15:57:02benjamin.petersonsetnosy: + benjamin.peterson
messages: + msg116805
2010-09-18 15:53:48BreamoreBoysetnosy: + BreamoreBoy
messages: + msg116804
2010-08-16 17:28:35daniel.urbansetnosy: + daniel.urban
2010-05-11 20:41:41terry.reedysetversions: + Python 2.7, - Python 2.5
2008-12-09 15:20:54thomasleesetnosy: + thomaslee
messages: + msg77424
2008-12-03 12:20:40gangesmastersetmessages: + msg76825
2008-01-20 20:01:53christian.heimessetpriority: high
2008-01-20 18:09:29pitrousetmessages: + msg61321
2008-01-20 17:29:07pitrousetnosy: + pitrou
messages: + msg61312
severity: normal -> major
2007-12-13 20:13:37gangesmastercreate