From 42fe1a2ff864c3f3491e6dab38b24e27db304633 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Mon, 17 Nov 2008 23:35:24 +0000 Subject: [PATCH] backport r67246 from the trunk --- Lib/test/test_descr.py | 59 +++++++++++++++++++++++++++++++++++------- Misc/NEWS | 2 ++ Objects/typeobject.c | 39 +++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 3c29017ea49..529939a2e81 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -1073,20 +1073,21 @@ def consistency_with_epg(): (EditableScrollablePane, ScrollablePane, EditablePane, Pane, ScrollingMixin, EditingMixin, object)) +def raises(exc, expected, callable, *args): + try: + callable(*args) + except exc, msg: + if not str(msg).startswith(expected): + raise TestFailed, "Message %r, expected %r" % (str(msg), + expected) + else: + raise TestFailed, "Expected %s" % exc + mro_err_msg = """Cannot create a consistent method resolution order (MRO) for bases """ def mro_disagreement(): if verbose: print "Testing error messages for MRO disagreement..." - def raises(exc, expected, callable, *args): - try: - callable(*args) - except exc, msg: - if not str(msg).startswith(expected): - raise TestFailed, "Message %r, expected %r" % (str(msg), - expected) - else: - raise TestFailed, "Expected %s" % exc class A(object): pass class B(A): pass class C(object): pass @@ -4171,6 +4172,45 @@ def methodwrapper(): vereq(t.__add__, (7,).__add__) vereq(hash(t.__add__), hash((7,).__add__)) +def test_getattr_hooks(): + # issue 4230 + 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, "attr", getattr, A(), "attr") + vereq(descr.counter, 1) + raises(AttributeError, "attr", getattr, B(), "attr") + vereq(descr.counter, 2) + raises(AttributeError, "attr", 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, "attr", getattr, EvilGetattribute(), "attr") + def notimplemented(): # all binary methods should be able to return a NotImplemented if verbose: @@ -4352,6 +4392,7 @@ def test_main(): methodwrapper() notimplemented() test_assign_slice() + test_getattr_hooks() if verbose: print "All OK" diff --git a/Misc/NEWS b/Misc/NEWS index 4c45e4fc50e..25bffa21e59 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -12,6 +12,8 @@ What's New in Python 2.5.3? Core and builtins ----------------- +- Issue #4230: If ``__getattr__`` is a descriptor, it now functions correctly. + - Issue #4048: The parser module now correctly validates relative imports. - Issue #4176: Fixed a crash when pickling an object which ``__reduce__`` diff --git a/Objects/typeobject.c b/Objects/typeobject.c index fd33fa47b76..4c032e77e45 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4737,6 +4737,24 @@ slot_tp_getattro(PyObject *self, PyObject *name) "(O)", name); } +static PyObject * +call_attribute(PyObject *self, PyObject *attr, PyObject *name) +{ + PyObject *res, *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 +4774,39 @@ slot_tp_getattr_hook(PyObject *self, PyObject *name) if (getattribute_str == NULL) return NULL; } + /* speed hack: we could use lookup_maybe, but that would resolve the + method fully 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 resolve the + method fully 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); 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); + else { + Py_INCREF(getattribute); + res = call_attribute(self, getattribute, name); + Py_DECREF(getattribute); + } if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) { PyErr_Clear(); - res = PyObject_CallFunctionObjArgs(getattr, self, name, NULL); + res = call_attribute(self, getattr, name); } + Py_DECREF(getattr); return res; }