From ab011fd9348ff419a6d583706eadd9acc17e79a9 Mon Sep 17 00:00:00 2001 From: stephan-hof Date: Tue, 24 Jan 2017 10:09:11 +0100 Subject: [PATCH 1/7] Add unittest for ComputedAttribute on class level. --- src/ComputedAttribute/tests.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/ComputedAttribute/tests.py b/src/ComputedAttribute/tests.py index 2763449..2ac5fab 100644 --- a/src/ComputedAttribute/tests.py +++ b/src/ComputedAttribute/tests.py @@ -73,6 +73,31 @@ def test_wrapper_support(): import unittest from doctest import DocTestSuite +from ExtensionClass import Base +from ComputedAttribute import ComputedAttribute + + +class TestComputedAttribute(unittest.TestCase): + def _construct_class(self, level): + class X(Base): + def _get_a(self): + return 1 + + a = ComputedAttribute(_get_a, level) + + return X + + def test_computed_attribute_on_class_level0(self): + x = self._construct_class(0)() + self.assertEqual(x.a, 1) + + def test_computed_attribute_on_class_level1(self): + x = self._construct_class(1)() + self.assertIsInstance(x.a, ComputedAttribute) + def test_suite(): - return unittest.TestSuite((DocTestSuite(),)) + suite = unittest.TestSuite() + suite.addTest(DocTestSuite()) + suite.addTest(unittest.makeSuite(TestComputedAttribute)) + return suite From 655776b08939061a3e044c65203d670c82f2cee9 Mon Sep 17 00:00:00 2001 From: stephan-hof Date: Tue, 24 Jan 2017 08:04:58 +0100 Subject: [PATCH 2/7] WIP: Add unmodified copy of _PyObject_GenericGetAttrWithDict from python2.7 --- src/ExtensionClass/_ExtensionClass.c | 136 +++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 19 deletions(-) diff --git a/src/ExtensionClass/_ExtensionClass.c b/src/ExtensionClass/_ExtensionClass.c index 43d03eb..0a9f840 100644 --- a/src/ExtensionClass/_ExtensionClass.c +++ b/src/ExtensionClass/_ExtensionClass.c @@ -44,36 +44,134 @@ of_get(PyObject *self, PyObject *inst, PyObject *cls) PyObject * Base_getattro(PyObject *obj, PyObject *name) { - int name_is_parent = 0; - PyObject* res = NULL; - PyObject* desc_res = NULL; + PyTypeObject *tp = Py_TYPE(obj); + PyObject *descr = NULL; + PyObject *res = NULL; + descrgetfunc f; + Py_ssize_t dictoffset; + PyObject **dictptr; + + if (!PyString_Check(name)){ +#ifdef Py_USING_UNICODE + /* The Unicode to string conversion is done here because the + existing tp_setattro slots expect a string object as name + and we wouldn't want to break those. */ + if (PyUnicode_Check(name)) { + name = PyUnicode_AsEncodedString(name, NULL, NULL); + if (name == NULL) + return NULL; + } + else +#endif + { + PyErr_Format(PyExc_TypeError, + "attribute name must be string, not '%.200s'", + Py_TYPE(name)->tp_name); + return NULL; + } + } + else + Py_INCREF(name); - res = PyObject_GenericGetAttr(obj, name); - if (res == NULL) { - return NULL; + if (tp->tp_dict == NULL) { + if (PyType_Ready(tp) < 0) + goto done; } - name_is_parent = PyObject_RichCompareBool(name, str__parent__, Py_EQ); - if (name_is_parent == -1) { - Py_DECREF(res); - return NULL; +#if 0 /* XXX this is not quite _PyType_Lookup anymore */ + /* Inline _PyType_Lookup */ + { + Py_ssize_t i, n; + PyObject *mro, *base, *dict; + + /* Look in tp_dict of types in MRO */ + mro = tp->tp_mro; + assert(mro != NULL); + assert(PyTuple_Check(mro)); + n = PyTuple_GET_SIZE(mro); + for (i = 0; i < n; i++) { + base = PyTuple_GET_ITEM(mro, i); + if (PyClass_Check(base)) + dict = ((PyClassObject *)base)->cl_dict; + else { + assert(PyType_Check(base)); + dict = ((PyTypeObject *)base)->tp_dict; + } + assert(dict && PyDict_Check(dict)); + descr = PyDict_GetItem(dict, name); + if (descr != NULL) + break; + } + } +#else + descr = _PyType_Lookup(tp, name); +#endif + + Py_XINCREF(descr); + + f = NULL; + if (descr != NULL && + PyType_HasFeature(descr->ob_type, Py_TPFLAGS_HAVE_CLASS)) { + f = descr->ob_type->tp_descr_get; + if (f != NULL && PyDescr_IsData(descr)) { + res = f(descr, obj, (PyObject *)obj->ob_type); + Py_DECREF(descr); + goto done; + } } - if (name_is_parent == 1) { - return res; + if (dict == NULL) { + /* Inline _PyObject_GetDictPtr */ + dictoffset = tp->tp_dictoffset; + if (dictoffset != 0) { + if (dictoffset < 0) { + Py_ssize_t tsize; + size_t size; + + tsize = ((PyVarObject *)obj)->ob_size; + if (tsize < 0) + tsize = -tsize; + size = _PyObject_VAR_SIZE(tp, tsize); + + dictoffset += (long)size; + assert(dictoffset > 0); + assert(dictoffset % SIZEOF_VOID_P == 0); + } + dictptr = (PyObject **) ((char *)obj + dictoffset); + dict = *dictptr; + } + } + if (dict != NULL) { + Py_INCREF(dict); + res = PyDict_GetItem(dict, name); + if (res != NULL) { + Py_INCREF(res); + Py_XDECREF(descr); + Py_DECREF(dict); + goto done; + } + Py_DECREF(dict); } - if (!PyObject_TypeCheck(Py_TYPE(res), &ExtensionClassType)) { - return res; + if (f != NULL) { + res = f(descr, obj, (PyObject *)Py_TYPE(obj)); + Py_DECREF(descr); + goto done; } - if (!Py_TYPE(res)->tp_descr_get) { - return res; + if (descr != NULL) { + res = descr; + /* descr was already increfed above */ + goto done; } - desc_res = Py_TYPE(res)->tp_descr_get(res, obj, (PyObject*)Py_TYPE(obj)); - Py_DECREF(res); - return desc_res; + PyErr_Format(PyExc_AttributeError, + "'%.50s' object has no attribute '%.400s'", + tp->tp_name, PyString_AS_STRING(name)); + done: + Py_DECREF(name); + return res; + } #include "pickle/pickle.c" From db6a6e019c8fddf70709703c0a1f1e7015294db3 Mon Sep 17 00:00:00 2001 From: stephan-hof Date: Tue, 24 Jan 2017 08:06:14 +0100 Subject: [PATCH 3/7] WIP: Get rid of dead code. --- src/ExtensionClass/_ExtensionClass.c | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/ExtensionClass/_ExtensionClass.c b/src/ExtensionClass/_ExtensionClass.c index 0a9f840..2bd4e49 100644 --- a/src/ExtensionClass/_ExtensionClass.c +++ b/src/ExtensionClass/_ExtensionClass.c @@ -78,35 +78,7 @@ Base_getattro(PyObject *obj, PyObject *name) goto done; } -#if 0 /* XXX this is not quite _PyType_Lookup anymore */ - /* Inline _PyType_Lookup */ - { - Py_ssize_t i, n; - PyObject *mro, *base, *dict; - - /* Look in tp_dict of types in MRO */ - mro = tp->tp_mro; - assert(mro != NULL); - assert(PyTuple_Check(mro)); - n = PyTuple_GET_SIZE(mro); - for (i = 0; i < n; i++) { - base = PyTuple_GET_ITEM(mro, i); - if (PyClass_Check(base)) - dict = ((PyClassObject *)base)->cl_dict; - else { - assert(PyType_Check(base)); - dict = ((PyTypeObject *)base)->tp_dict; - } - assert(dict && PyDict_Check(dict)); - descr = PyDict_GetItem(dict, name); - if (descr != NULL) - break; - } - } -#else descr = _PyType_Lookup(tp, name); -#endif - Py_XINCREF(descr); f = NULL; From 6617fe128c1591928013af0e7bdc9bba64403849 Mon Sep 17 00:00:00 2001 From: stephan-hof Date: Tue, 24 Jan 2017 08:14:41 +0100 Subject: [PATCH 4/7] WIP: Do not inline _PyObject_GetDictPtr. Use as much API from python as possible (even if its declared private). Its better than copy&paste, which breaks if the internal implementation of python changes. --- src/ExtensionClass/_ExtensionClass.c | 34 ++++++---------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/ExtensionClass/_ExtensionClass.c b/src/ExtensionClass/_ExtensionClass.c index 2bd4e49..ab939d7 100644 --- a/src/ExtensionClass/_ExtensionClass.c +++ b/src/ExtensionClass/_ExtensionClass.c @@ -48,7 +48,6 @@ Base_getattro(PyObject *obj, PyObject *name) PyObject *descr = NULL; PyObject *res = NULL; descrgetfunc f; - Py_ssize_t dictoffset; PyObject **dictptr; if (!PyString_Check(name)){ @@ -92,37 +91,18 @@ Base_getattro(PyObject *obj, PyObject *name) } } - if (dict == NULL) { - /* Inline _PyObject_GetDictPtr */ - dictoffset = tp->tp_dictoffset; - if (dictoffset != 0) { - if (dictoffset < 0) { - Py_ssize_t tsize; - size_t size; - - tsize = ((PyVarObject *)obj)->ob_size; - if (tsize < 0) - tsize = -tsize; - size = _PyObject_VAR_SIZE(tp, tsize); - - dictoffset += (long)size; - assert(dictoffset > 0); - assert(dictoffset % SIZEOF_VOID_P == 0); - } - dictptr = (PyObject **) ((char *)obj + dictoffset); - dict = *dictptr; - } - } - if (dict != NULL) { - Py_INCREF(dict); - res = PyDict_GetItem(dict, name); + dictptr = _PyObject_GetDictPtr(obj); + + if (dictptr && *dictptr) { + Py_INCREF(*dictptr); + res = PyDict_GetItem(*dictptr, name); if (res != NULL) { Py_INCREF(res); Py_XDECREF(descr); - Py_DECREF(dict); + Py_DECREF(*dictptr); goto done; } - Py_DECREF(dict); + Py_DECREF(*dictptr); } if (f != NULL) { From 95053001b7c801233a1ae508ac2929e61da4a724 Mon Sep 17 00:00:00 2001 From: stephan-hof Date: Tue, 24 Jan 2017 08:38:12 +0100 Subject: [PATCH 5/7] WIP: Make it working under python3 and python2. --- src/ExtensionClass/_ExtensionClass.c | 14 +++++++++++--- src/ExtensionClass/_compat.h | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ExtensionClass/_ExtensionClass.c b/src/ExtensionClass/_ExtensionClass.c index ab939d7..0a23de0 100644 --- a/src/ExtensionClass/_ExtensionClass.c +++ b/src/ExtensionClass/_ExtensionClass.c @@ -50,7 +50,8 @@ Base_getattro(PyObject *obj, PyObject *name) descrgetfunc f; PyObject **dictptr; - if (!PyString_Check(name)){ + if (!NATIVE_CHECK(name)) { +#ifndef PY3K #ifdef Py_USING_UNICODE /* The Unicode to string conversion is done here because the existing tp_setattro slots expect a string object as name @@ -61,6 +62,7 @@ Base_getattro(PyObject *obj, PyObject *name) return NULL; } else +#endif #endif { PyErr_Format(PyExc_TypeError, @@ -81,8 +83,7 @@ Base_getattro(PyObject *obj, PyObject *name) Py_XINCREF(descr); f = NULL; - if (descr != NULL && - PyType_HasFeature(descr->ob_type, Py_TPFLAGS_HAVE_CLASS)) { + if (descr != NULL && HAS_TP_DESCR_GET(descr)) { f = descr->ob_type->tp_descr_get; if (f != NULL && PyDescr_IsData(descr)) { res = f(descr, obj, (PyObject *)obj->ob_type); @@ -117,9 +118,16 @@ Base_getattro(PyObject *obj, PyObject *name) goto done; } +#ifdef PY3K + PyErr_Format(PyExc_AttributeError, + "'%.50s' object has no attribute '%U'", + tp->tp_name, name); +#else PyErr_Format(PyExc_AttributeError, "'%.50s' object has no attribute '%.400s'", tp->tp_name, PyString_AS_STRING(name)); +#endif + done: Py_DECREF(name); return res; diff --git a/src/ExtensionClass/_compat.h b/src/ExtensionClass/_compat.h index 9f4cd0b..9249333 100644 --- a/src/ExtensionClass/_compat.h +++ b/src/ExtensionClass/_compat.h @@ -33,6 +33,8 @@ #define INT_CHECK(x) PyLong_Check(x) #define INT_AS_LONG(x) PyLong_AS_LONG(x) +#define HAS_TP_DESCR_GET(ob) 1 + #else #define INTERN PyString_InternFromString #define INTERN_INPLACE PyString_InternInPlace @@ -44,6 +46,8 @@ #define INT_FROM_LONG(x) PyInt_FromLong(x) #define INT_CHECK(x) PyInt_Check(x) #define INT_AS_LONG(x) PyInt_AS_LONG(x) + +#define HAS_TP_DESCR_GET(ob) PyType_HasFeature(Py_TYPE(ob), Py_TPFLAGS_HAVE_CLASS) #endif #endif From 54320cd7c5463b103d81cb521eb45c20f11ffc9f Mon Sep 17 00:00:00 2001 From: stephan-hof Date: Tue, 24 Jan 2017 09:14:54 +0100 Subject: [PATCH 6/7] Bring back our extension to call of_get of an attribute inside the instance dict. --- src/ExtensionClass/_ExtensionClass.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ExtensionClass/_ExtensionClass.c b/src/ExtensionClass/_ExtensionClass.c index 0a23de0..0750b2e 100644 --- a/src/ExtensionClass/_ExtensionClass.c +++ b/src/ExtensionClass/_ExtensionClass.c @@ -101,6 +101,24 @@ Base_getattro(PyObject *obj, PyObject *name) Py_INCREF(res); Py_XDECREF(descr); Py_DECREF(*dictptr); + + /* CHANGED! If the tp_descr_get of res is of_get, then call it. */ + if (PyObject_TypeCheck(Py_TYPE(res), &ExtensionClassType)) { + if (Py_TYPE(res)->tp_descr_get) { + int name_is_parent = PyObject_RichCompareBool(name, str__parent__, Py_EQ); + + if (name_is_parent == 0) { + PyObject *tres = Py_TYPE(res)->tp_descr_get(res, obj, (PyObject*)Py_TYPE(obj)); + Py_DECREF(res); + res = tres; + } + else if (name_is_parent == -1) { + PyErr_Clear(); + } + } + } + /* End of change. */ + goto done; } Py_DECREF(*dictptr); From cd664a9bb23290334f0513b34546ace796d65b17 Mon Sep 17 00:00:00 2001 From: stephan-hof Date: Wed, 25 Jan 2017 09:20:45 +0100 Subject: [PATCH 7/7] Fix the python version of Base_getattro. --- src/ExtensionClass/__init__.py | 40 ++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/ExtensionClass/__init__.py b/src/ExtensionClass/__init__.py index d7247bc..f6e302e 100644 --- a/src/ExtensionClass/__init__.py +++ b/src/ExtensionClass/__init__.py @@ -98,6 +98,7 @@ class init called 1 """ +import inspect import os import sys @@ -219,13 +220,38 @@ def __setattr__(self, name, value): # to care or worry about using super(): it's always object. def Base_getattro(self, name): - res = object.__getattribute__(self, name) - # If it's a descriptor for something besides __parent__, call it. - if name != '__parent__' and isinstance(res, Base): - descr_get = getattr(res, '__get__', None) - if descr_get is not None: - res = descr_get(self, type(self)) - return res + descr = None + + for base in type(self).__mro__: + if name in base.__dict__: + descr = base.__dict__[name] + break + + if descr is not None and inspect.isdatadescriptor(base): + return descr.__get__(self, type(self)) + + try: + # Don't do self.__dict__ otherwise you get recursion. + inst_dict = object.__getattribute__(self, '__dict__') + except AttributeError: + pass + else: + if name in inst_dict: + descr = inst_dict[name] + # If the tp_descr_get of res is of_get, then call it. + if name == '__parent__' or not isinstance(descr, Base): + return descr + + if descr is not None: + descr_get = getattr(descr, '__get__', None) + if descr_get is None: + return descr + + return descr_get(self, type(self)) + + raise AttributeError( + "'%.50s' object has not attribute '%s'", + type(self).__name__, name) def _slotnames(self):