Index: Lib/test/test_descr.py =================================================================== --- Lib/test/test_descr.py (revision 67240) +++ Lib/test/test_descr.py (working copy) @@ -4255,6 +4255,57 @@ c[1:2] = 3 vereq(c.value, 3) + +def test_getattr_hooks(): + # issue 4230 + + def raises(exc, func, *args): + try: + func(*args) + except exc: + return + else: + raise TestFailed("%r didn't raise %r when called with %r" % + (func, exc, args)) + + class Descriptor(object): + counter = 0 + def __get__(self, obj, objtype=None): + def getter(name): + self.counter += 1 + raise AttributeError(name) + return getter + + descr = Descriptor() + class A(object): + __getattribute__ = descr + class B(object): + __getattr__ = descr + class C(object): + __getattribute__ = descr + __getattr__ = descr + + raises(AttributeError, getattr, A(), "attr") + vereq(descr.counter, 1) + raises(AttributeError, getattr, B(), "attr") + vereq(descr.counter, 2) + raises(AttributeError, getattr, C(), "attr") + vereq(descr.counter, 4) + + import gc + class EvilGetattribute(object): + # This used to segfault + def __getattr__(self, name): + raise AttributeError(name) + def __getattribute__(self, name): + del EvilGetattribute.__getattr__ + for i in range(5): + gc.collect() + raise AttributeError(name) + + raises(AttributeError, getattr, EvilGetattribute(), "attr") + + def test_main(): weakref_segfault() # Must be first, somehow wrapper_segfault() @@ -4352,6 +4403,7 @@ methodwrapper() notimplemented() test_assign_slice() + test_getattr_hooks() if verbose: print "All OK" Index: Objects/typeobject.c =================================================================== --- Objects/typeobject.c (revision 67240) +++ Objects/typeobject.c (working copy) @@ -4737,6 +4737,25 @@ "(O)", name); } + static PyObject * +call_attribute(PyObject *attr, PyObject *self, PyObject *name) +{ + PyObject *res; + PyObject *descr = NULL; + descrgetfunc f = attr->ob_type->tp_descr_get; + + if (f != NULL) { + descr = f(attr, self, (PyObject *)(self->ob_type)); + if (descr == NULL) + return NULL; + else + attr = descr; + } + res = PyObject_CallFunctionObjArgs(attr, name, NULL); + Py_XDECREF(descr); + return res; +} + static PyObject * slot_tp_getattr_hook(PyObject *self, PyObject *name) { @@ -4756,24 +4775,37 @@ if (getattribute_str == NULL) return NULL; } + /* speed hack: we could use lookup_maybe, but that would create + a method for each attribute lookup for classes with __getattr__, + even when the attribute is present. So we use _PyType_Lookup and + create the method only when needed, with call_attribute. */ getattr = _PyType_Lookup(tp, getattr_str); if (getattr == NULL) { /* No __getattr__ hook: use a simpler dispatcher */ tp->tp_getattro = slot_tp_getattro; return slot_tp_getattro(self, name); } + Py_INCREF(getattr); + /* speed hack: we could use lookup_maybe, but that would create + a method for each attribute lookup for classes with __getattr__, + even when self has the default __getattribute__ method. So we use + _PyType_Lookup and create the method only when needed, + with call_attribute. */ getattribute = _PyType_Lookup(tp, getattribute_str); + Py_XINCREF(getattribute); if (getattribute == NULL || (getattribute->ob_type == &PyWrapperDescr_Type && ((PyWrapperDescrObject *)getattribute)->d_wrapped == (void *)PyObject_GenericGetAttr)) res = PyObject_GenericGetAttr(self, name); else - res = PyObject_CallFunctionObjArgs(getattribute, self, name, NULL); + res = call_attribute(getattribute, self, name); + Py_XDECREF(getattribute); if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) { PyErr_Clear(); - res = PyObject_CallFunctionObjArgs(getattr, self, name, NULL); + res = call_attribute(getattr, self, name); } + Py_DECREF(getattr); return res; }