diff --git a/src/Acquisition/_Acquisition.c b/src/Acquisition/_Acquisition.c index b3c39cbab605754475fa055960fd12ea5ff2c2e5..ff66620193cb47c9a33d9c8891278a0c26b28fd7 100644 --- a/src/Acquisition/_Acquisition.c +++ b/src/Acquisition/_Acquisition.c @@ -447,6 +447,64 @@ err: return -1; } +static PyObject * +Wrapper_GetAttr(PyObject *self, PyObject *attr_name, PyObject *orig) +{ + /* This function retrieves an attribute from an object by PyObject_GetAttr. + + The main difference between Wrapper_GetAttr and PyObject_GetAttr is that + Wrapper_GetAttr calls _aq_dynamic to generate an attribute dynamically, if + the attribute is not found. + */ + PyObject *r, *v, *tb; + PyObject *d, *m; + PyObject *o; + + if (isWrapper (self)) + o = WRAPPER(self)->obj; + else + o = self; + + /* Try to get an attribute in the normal way first. */ + r = PyObject_GetAttr(o, attr_name); + if (r) + return r; + + /* If an unexpected error happens, return immediately. */ + PyErr_Fetch(&r,&v,&tb); + if (r != PyExc_AttributeError) + { + PyErr_Restore(r,v,tb); + return NULL; + } + + /* Try to get _aq_dynamic. */ + m = PyObject_GetAttrString(o, "_aq_dynamic"); + if (! m) { + PyErr_Restore(r,v,tb); + return NULL; + } + + /* Call _aq_dynamic in the context of the original acquisition wrapper. */ + if (PyECMethod_Check(m) && PyECMethod_Self(m)==o) + ASSIGN(m,PyECMethod_New(m,OBJECT(self))); + else if (has__of__(m)) ASSIGN(m,__of__(m,OBJECT(self))); + d = PyObject_CallFunction(m, "O", attr_name); + Py_DECREF(m); + + /* In the case of None, assume that the attribute is not found. */ + if (d == Py_None) { + Py_DECREF(d); + PyErr_Restore(r,v,tb); + return NULL; + } + + Py_XDECREF(r); + Py_XDECREF(v); + Py_XDECREF(tb); + return d; +} + static PyObject * Wrapper_acquire(Wrapper *self, PyObject *oname, PyObject *filter, PyObject *extra, PyObject *orig, @@ -551,8 +609,8 @@ Wrapper_findattr(Wrapper *self, PyObject *oname, Py_XDECREF(r); Py_XDECREF(v); Py_XDECREF(tb); r=NULL; } - /* normal attribute lookup */ - else if ((r=PyObject_GetAttr(self->obj,oname))) + /* Give _aq_dynamic a chance, then normal attribute lookup */ + else if ((r=Wrapper_GetAttr(OBJECT(self),oname,orig))) { if (r==Acquired) { @@ -676,7 +734,7 @@ Wrapper_acquire(Wrapper *self, PyObject *oname, Py_XDECREF(r); Py_XDECREF(v); Py_XDECREF(tb); r=NULL; - if ((r=PyObject_GetAttr(self->container,oname))) { + if ((r=Wrapper_GetAttr(self->container,oname,orig))) { if (r == Acquired) { Py_DECREF(r); } @@ -713,7 +771,7 @@ static PyObject * Wrapper_getattro(Wrapper *self, PyObject *oname) { if (self->obj || self->container) - return Wrapper_findattr(self, oname, NULL, NULL, NULL, 1, 1, 0, 0); + return Wrapper_findattr(self, oname, NULL, NULL, OBJECT(self), 1, 1, 0, 0); /* Maybe we are getting initialized? */ return Py_FindAttr(OBJECT(self),oname); @@ -737,7 +795,7 @@ Xaq_getattro(Wrapper *self, PyObject *oname) return Py_FindAttr(OBJECT(self),oname); if (self->obj || self->container) - return Wrapper_findattr(self, oname, NULL, NULL, NULL, 1, 0, 0, 0); + return Wrapper_findattr(self, oname, NULL, NULL, OBJECT(self), 1, 0, 0, 0); /* Maybe we are getting initialized? */ return Py_FindAttr(OBJECT(self),oname); diff --git a/src/Acquisition/test_dynamic_acquisition.py b/src/Acquisition/test_dynamic_acquisition.py new file mode 100644 index 0000000000000000000000000000000000000000..4658664d25d078865e2155629c4ccfa0f69d1020 --- /dev/null +++ b/src/Acquisition/test_dynamic_acquisition.py @@ -0,0 +1,160 @@ +############################################################################## +# +# Copyright (c) 1996-2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +import Acquisition + +def checkContext(self, o): + # Python equivalent to aq_inContextOf + from Acquisition import aq_base, aq_parent, aq_inner + subob = self + o = aq_base(o) + while 1: + if aq_base(subob) is o: + return True + self = aq_inner(subob) + if self is None: break + subob = aq_parent(self) + if subob is None: break + return False + +class B(Acquisition.Implicit): + color='red' + + def __init__(self, name='b'): + self.name = name + + def _aq_dynamic(self, attr): + if attr == 'bonjour': return None + + def dynmethod(): + chain = ' <- '.join(repr(obj) for obj in Acquisition.aq_chain(self)) + print repr(self) + '.' + attr + print 'chain:', chain + + return dynmethod + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.name) + +class A(Acquisition.Implicit): + + def __init__(self, name='a'): + self.name = name + + def hi(self): + print self, self.color + + def _aq_dynamic(self, attr): + return None + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.name) + +def test_dynamic(): + r''' + The _aq_dynamic functionality allows an object to dynamically provide an + attribute. + + If an object doesn't have an attribute, Acquisition checks to see if the + object has a _aq_dynamic method, which is then called. It is functionally + equivalent to __getattr__, but _aq_dynamic is called with 'self' as the + acquisition wrapped object where as __getattr__ is called with self as the + unwrapped object. + + Let's see how this works. In the examples below, the A class defines + '_aq_dynamic', but returns 'None' for all attempts, which means that no new + attributes should be generated dynamically. It also doesn't define 'color' + attribute, even though it uses it in the 'hi' method. + + >>> A().hi() + Traceback (most recent call last): + ... + AttributeError: color + + The class B, on the other hand, generates all attributes dynamically, + except if it is called 'bonjour'. + + First we need to check that, even if an object provides '_aq_dynamic', + "regular" Aquisition attribute access should still work: + + >>> b=B() + >>> b.a=A() + >>> b.a.hi() + A('a') red + >>> b.a.color='green' + >>> b.a.hi() + A('a') green + + Now, let's see some dynamically generated action. B does not define a + 'salut' method, but remember that it dynamically generates a method for + every attribute access: + + >>> b.a.salut() + B('b').salut + chain: B('b') + + >>> a=A('a1') + >>> a.b=B('b1') + >>> a.b.salut() + B('b1').salut + chain: B('b1') <- A('a1') + + >>> b.a.bonjour() + Traceback (most recent call last): + ... + AttributeError: bonjour + + >>> a.b.bonjour() + Traceback (most recent call last): + ... + AttributeError: bonjour + + ''' + +def test_wrapper_comparissons(): + r''' + + Test wrapper comparisons in presence of _aq_dynamic + + >>> b=B() + >>> b.a=A() + >>> foo = b.a + >>> bar = b.a + >>> assert( foo == bar ) + >>> c = A('c') + >>> b.c = c + >>> b.c.d = c + >>> b.c.d == c + True + >>> b.c.d == b.c + True + >>> b.c == c + True + + Test contextuality in presence of _aq_dynamic + + >>> checkContext(b.c, b) + True + >>> checkContext(b.c, b.a) + False + + >>> assert b.a.aq_inContextOf(b) + >>> assert b.c.aq_inContextOf(b) + >>> assert b.c.d.aq_inContextOf(b) + >>> assert b.c.d.aq_inContextOf(c) + >>> assert b.c.d.aq_inContextOf(b.c) + >>> assert not b.c.aq_inContextOf(foo) + >>> assert not b.c.aq_inContextOf(b.a) + >>> assert not b.a.aq_inContextOf('somestring') +''' + diff --git a/src/Acquisition/tests.py b/src/Acquisition/tests.py index 6f6f6d56d5fb503421ea24822867eee5531fd33b..4348e1e709cf4a51fb83b431c2568faa55f4f44d 100644 --- a/src/Acquisition/tests.py +++ b/src/Acquisition/tests.py @@ -2588,6 +2588,7 @@ class TestUnicode(unittest.TestCase): def test_suite(): return unittest.TestSuite(( DocTestSuite(), + DocTestSuite('Acquisition.test_dynamic_acquisition'), DocFileSuite('README.txt', package='Acquisition'), unittest.makeSuite(TestParent), unittest.makeSuite(TestAcquire),