Commit d37122e4 authored by Tres Seaver's avatar Tres Seaver

Merge pull request #10 from NextThought/object_getattribute_wrapped_types

Make object.__getattribute__ work in methods wrapped with an acquisition wrapper.
parents 6f20ea96 38ae2cd0
...@@ -4,7 +4,15 @@ Changelog ...@@ -4,7 +4,15 @@ Changelog
4.2.2 (unreleased) 4.2.2 (unreleased)
------------------ ------------------
- TBD - Make the pure-Python Acquirer objects cooperatively use the
superclass ``__getattribute__`` method, like the C implementation.
See https://github.com/zopefoundation/Acquisition/issues/7.
- The pure-Python implicit acquisition wrapper allows wrapped objects
to use ``object.__getattribute__(self, name)``. This differs from
the C implementation, but is important for compatibility with the
pure-Python versions of libraries like ``persistent``. See
https://github.com/zopefoundation/Acquisition/issues/9.
4.2.1 (2015-04-23) 4.2.1 (2015-04-23)
------------------ ------------------
......
...@@ -2,10 +2,12 @@ from __future__ import absolute_import, print_function ...@@ -2,10 +2,12 @@ from __future__ import absolute_import, print_function
# pylint:disable=W0212,R0911,R0912 # pylint:disable=W0212,R0911,R0912
import os import os
import operator import operator
import sys import sys
import types import types
import weakref
import ExtensionClass import ExtensionClass
...@@ -39,17 +41,30 @@ def _apply_filter(predicate, inst, name, result, extra, orig): ...@@ -39,17 +41,30 @@ def _apply_filter(predicate, inst, name, result, extra, orig):
return predicate(orig, inst, name, result, extra) return predicate(orig, inst, name, result, extra)
if sys.version_info < (3,): if sys.version_info < (3,):
import copy_reg
def _rebound_method(method, wrapper): def _rebound_method(method, wrapper):
"""Returns a version of the method with self bound to `wrapper`""" """Returns a version of the method with self bound to `wrapper`"""
if isinstance(method, types.MethodType): if isinstance(method, types.MethodType):
method = types.MethodType(method.im_func, wrapper, method.im_class) method = types.MethodType(method.im_func, wrapper, method.im_class)
return method return method
exec("""def _reraise(tp, value, tb=None):
raise tp, value, tb
""")
else: # pragma: no cover (python 2 is currently our reference) else: # pragma: no cover (python 2 is currently our reference)
import copyreg as copy_reg
def _rebound_method(method, wrapper): def _rebound_method(method, wrapper):
"""Returns a version of the method with self bound to `wrapper`""" """Returns a version of the method with self bound to `wrapper`"""
if isinstance(method, types.MethodType): if isinstance(method, types.MethodType):
method = types.MethodType(method.__func__, wrapper) method = types.MethodType(method.__func__, wrapper)
return method return method
def _reraise(tp, value, tb=None):
if value is None:
value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
### ###
# Wrapper object protocol, mostly ported from C directly # Wrapper object protocol, mostly ported from C directly
...@@ -283,16 +298,58 @@ def _Wrapper_findattr(wrapper, name, ...@@ -283,16 +298,58 @@ def _Wrapper_findattr(wrapper, name,
_NOT_GIVEN = object() # marker _NOT_GIVEN = object() # marker
_OGA = object.__getattribute__
# Map from object types with slots to their generated, derived
# types (or None if no derived type is needed)
_wrapper_subclass_cache = weakref.WeakKeyDictionary()
def _make_wrapper_subclass_if_needed(cls, obj, container):
# If the type of an object to be wrapped has __slots__, then we
# must create a wrapper subclass that has descriptors for those
# same slots. In this way, its methods that use object.__getattribute__
# directly will continue to work, even when given an instance of _Wrapper
if getattr(cls, '_Wrapper__DERIVED', False):
return None
type_obj = type(obj)
wrapper_subclass = _wrapper_subclass_cache.get(type_obj, _NOT_GIVEN)
if wrapper_subclass is _NOT_GIVEN:
slotnames = copy_reg._slotnames(type_obj)
if slotnames and not isinstance(obj, _Wrapper):
new_type_dict = {'_Wrapper__DERIVED': True}
def _make_property(slotname):
return property(lambda s: getattr(s._obj, slotname),
lambda s, v: setattr(s._obj, slotname, v),
lambda s: delattr(s._obj, slotname))
for slotname in slotnames:
new_type_dict[slotname] = _make_property(slotname)
new_type = type(cls.__name__ + '_' + type_obj.__name__,
(cls,),
new_type_dict)
else:
new_type = None
wrapper_subclass = _wrapper_subclass_cache[type_obj] = new_type
return wrapper_subclass
class _Wrapper(ExtensionClass.Base): class _Wrapper(ExtensionClass.Base):
__slots__ = ('_obj','_container',) __slots__ = ('_obj','_container', '__dict__')
_IS_IMPLICIT = None _IS_IMPLICIT = None
def __new__(cls, obj, container): def __new__(cls, obj, container):
inst = super(_Wrapper,cls).__new__(cls) wrapper_subclass = _make_wrapper_subclass_if_needed(cls, obj, container)
if wrapper_subclass:
inst = wrapper_subclass(obj, container)
else:
inst = super(_Wrapper,cls).__new__(cls)
inst._obj = obj inst._obj = obj
inst._container = container inst._container = container
if hasattr(obj, '__dict__') and not isinstance(obj, _Wrapper):
# Make our __dict__ refer to the same dict
# as the other object, so that if it has methods that
# use `object.__getattribute__` they still work. Note that because we have
# slots, we won't interfere with the contents of that dict
object.__setattr__(inst, '__dict__', obj.__dict__)
return inst return inst
def __init__(self, obj, container): def __init__(self, obj, container):
...@@ -325,11 +382,11 @@ class _Wrapper(ExtensionClass.Base): ...@@ -325,11 +382,11 @@ class _Wrapper(ExtensionClass.Base):
def __getattribute__(self, name): def __getattribute__(self, name):
if name in ('_obj', '_container'): if name in ('_obj', '_container'):
return object.__getattribute__(self, name) return _OGA(self, name)
if self._obj is not None or self._container is not None: if _OGA(self, '_obj') is not None or _OGA(self, '_container') is not None:
return _Wrapper_findattr(self, name, None, None, None, return _Wrapper_findattr(self, name, None, None, None,
True, type(self)._IS_IMPLICIT, False, False) True, type(self)._IS_IMPLICIT, False, False)
return object.__getattribute__(self, name) return _OGA(self, name)
def __of__(self, parent): def __of__(self, parent):
# Based on __of__ in the C code; # Based on __of__ in the C code;
...@@ -684,11 +741,15 @@ class _Acquirer(ExtensionClass.Base): ...@@ -684,11 +741,15 @@ class _Acquirer(ExtensionClass.Base):
def __getattribute__(self, name): def __getattribute__(self, name):
try: try:
return ExtensionClass.Base.__getattribute__(self, name) return super(_Acquirer, self).__getattribute__(name)
except AttributeError: except AttributeError:
# the doctests have very specific error message # the doctests have very specific error message
# requirements # requirements (but at least we can preserve the traceback)
raise AttributeError(name) _, _, tb = sys.exc_info()
try:
_reraise(AttributeError, AttributeError(name), tb)
finally:
del tb
def __of__(self, context): def __of__(self, context):
return type(self)._Wrapper(self, context) return type(self)._Wrapper(self, context)
......
...@@ -3124,6 +3124,125 @@ class TestAcquire(unittest.TestCase): ...@@ -3124,6 +3124,125 @@ class TestAcquire(unittest.TestCase):
found = self.acquire(self.a.b.c, AQ_PARENT) found = self.acquire(self.a.b.c, AQ_PARENT)
self.assertTrue(found.aq_self is self.a.b.aq_self) self.assertTrue(found.aq_self is self.a.b.aq_self)
class TestCooperativeBase(unittest.TestCase):
def _make_acquirer(self, kind):
from ExtensionClass import Base
class ExtendsBase(Base):
def __getattribute__(self, name):
if name == 'magic':
return 42
return super(ExtendsBase,self).__getattribute__(name)
class Acquirer(kind, ExtendsBase):
pass
return Acquirer()
def _check___getattribute___is_cooperative(self, acquirer):
self.assertEqual(getattr(acquirer, 'magic'), 42)
def test_implicit___getattribute__is_cooperative(self):
self._check___getattribute___is_cooperative(self._make_acquirer(Acquisition.Implicit))
def test_explicit___getattribute__is_cooperative(self):
self._check___getattribute___is_cooperative(self._make_acquirer(Acquisition.Explicit))
if 'Acquisition._Acquisition' not in sys.modules:
# Implicitly wrapping an object that uses object.__getattribute__
# in its implementation of __getattribute__ doesn't break.
# This can arise with the `persistent` library or other
# "base" classes.
# The C implementation doesn't directly support this; however,
# it is used heavily in the Python implementation of Persistent.
class TestImplicitWrappingGetattribute(unittest.TestCase):
def test_object_getattribute_in_rebound_method_with_slots(self):
class Persistent(object):
__slots__ = ('__flags',)
def __init__(self):
self.__flags = 42
def get_flags(self):
return object.__getattribute__(self, '_Persistent__flags')
wrapped = Persistent()
wrapper = Acquisition.ImplicitAcquisitionWrapper(wrapped, None)
self.assertEqual(wrapped.get_flags(), wrapper.get_flags())
# Changing it is not reflected in the wrapper's dict (this is an
# implementation detail)
wrapper._Persistent__flags = -1
self.assertEqual(wrapped.get_flags(), -1)
self.assertEqual(wrapped.get_flags(), wrapper.get_flags())
wrapper_dict = object.__getattribute__(wrapper, '__dict__')
self.assertFalse('_Persistent__flags' in wrapper_dict)
def test_type_with_slots_reused(self):
class Persistent(object):
__slots__ = ('__flags',)
def __init__(self):
self.__flags = 42
def get_flags(self):
return object.__getattribute__(self, '_Persistent__flags')
wrapped = Persistent()
wrapper = Acquisition.ImplicitAcquisitionWrapper(wrapped, None)
wrapper2 = Acquisition.ImplicitAcquisitionWrapper(wrapped, None)
self.assertTrue( type(wrapper) is type(wrapper2))
def test_object_getattribute_in_rebound_method_with_dict(self):
class Persistent(object):
def __init__(self):
self.__flags = 42
def get_flags(self):
return object.__getattribute__(self, '_Persistent__flags')
wrapped = Persistent()
wrapper = Acquisition.ImplicitAcquisitionWrapper(wrapped, None)
self.assertEqual(wrapped.get_flags(), wrapper.get_flags())
# Changing it is also reflected in both dicts (this is an
# implementation detail)
wrapper._Persistent__flags = -1
self.assertEqual(wrapped.get_flags(), -1)
self.assertEqual(wrapped.get_flags(), wrapper.get_flags())
wrapper_dict = object.__getattribute__(wrapper, '__dict__')
self.assertTrue('_Persistent__flags' in wrapper_dict)
def test_object_getattribute_in_rebound_method_with_slots_and_dict(self):
class Persistent(object):
__slots__ = ('__flags', '__dict__')
def __init__(self):
self.__flags = 42
self.__oid = 'oid'
def get_flags(self):
return object.__getattribute__(self, '_Persistent__flags')
def get_oid(self):
return object.__getattribute__(self, '_Persistent__oid')
wrapped = Persistent()
wrapper = Acquisition.ImplicitAcquisitionWrapper(wrapped, None)
self.assertEqual(wrapped.get_flags(), wrapper.get_flags())
self.assertEqual(wrapped.get_oid(), wrapper.get_oid())
class TestUnicode(unittest.TestCase): class TestUnicode(unittest.TestCase):
...@@ -3537,6 +3656,7 @@ def test_suite(): ...@@ -3537,6 +3656,7 @@ def test_suite():
unittest.makeSuite(TestAcquire), unittest.makeSuite(TestAcquire),
unittest.makeSuite(TestUnicode), unittest.makeSuite(TestUnicode),
unittest.makeSuite(TestProxying), unittest.makeSuite(TestProxying),
unittest.makeSuite(TestCooperativeBase),
] ]
# This file is only available in a source checkout, skip it # This file is only available in a source checkout, skip it
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment