Commit 37e9ea6a authored by Tres Seaver's avatar Tres Seaver

Merge pull request #6 from NextThought/python-provided

Handle descriptors defined in PyProxyBase subclasses like C
parents 65ebb9d1 7d0d502e
...@@ -4,7 +4,9 @@ Changes ...@@ -4,7 +4,9 @@ Changes
4.1.5 (unreleased) 4.1.5 (unreleased)
------------------ ------------------
- TBD - The pure Python implementation handles descriptors defined in
subclasses like the C version. See
https://github.com/zopefoundation/zope.proxy/issues/5.
4.1.4 (2014-03-19) 4.1.4 (2014-03-19)
------------------ ------------------
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"""More convenience functions for dealing with proxies. """More convenience functions for dealing with proxies.
""" """
import operator import operator
import os
import pickle import pickle
import sys import sys
...@@ -33,13 +34,45 @@ def ProxyIterator(p): ...@@ -33,13 +34,45 @@ def ProxyIterator(p):
_MARKER = object() _MARKER = object()
class PyProxyBase(object): def _WrapperType_Lookup(type_, name):
"""Reference implementation.
""" """
__slots__ = ('_wrapped', ) Looks up information in class dictionaries in MRO
order, ignoring the proxy type itself.
Returns the first found object, or _MARKER
"""
for base in type_.mro():
if base is AbstractPyProxyBase:
continue
res = base.__dict__.get(name, _MARKER)
if res is not _MARKER:
return res
return _MARKER
def _get_wrapped(self):
"""
Helper method to access the wrapped object.
"""
return super(AbstractPyProxyBase, self).__getattribute__('_wrapped')
class AbstractPyProxyBase(object):
"""
A reference implementation that cannot be instantiated. Most users
will want to use :class:`PyProxyBase`.
This type is intended to be used in multiple-inheritance
scenarios, where another super class already has defined
``__slots__``. In order to subclass both that class and this
class, you must include the ``_wrapped`` value in your own
``__slots__`` definition (or else you will get the infamous
TypeError: "multiple bases have instance lay-out conflicts")
"""
__slots__ = ()
def __new__(cls, value): def __new__(cls, value=None):
inst = super(PyProxyBase, cls).__new__(cls) # Some subclasses (zope.security.proxy) fail to pass the object
inst = super(AbstractPyProxyBase, cls).__new__(cls)
inst._wrapped = value inst._wrapped = value
return inst return inst
...@@ -92,35 +125,56 @@ class PyProxyBase(object): ...@@ -92,35 +125,56 @@ class PyProxyBase(object):
# Attribute protocol # Attribute protocol
def __getattribute__(self, name): def __getattribute__(self, name):
wrapped = super(PyProxyBase, self).__getattribute__('_wrapped') # Try to avoid accessing the _wrapped value until we need to.
# We don't know how subclasses may be storing it
# (e.g., persistent subclasses)
if name == '_wrapped': if name == '_wrapped':
return wrapped return _get_wrapped(self)
try:
mine = super(PyProxyBase, self).__getattribute__(name) if name == '__class__':
except AttributeError: # __class__ is special cased in the C implementation
mine = _MARKER return _get_wrapped(self).__class__
else:
if isinstance(mine, PyNonOverridable): #pragma NO COVER PyPy if name in ('__reduce__', '__reduce_ex__'):
return mine.desc.__get__(self) # These things we specifically override and no one
try: # can stop us, not even a subclass
return getattr(wrapped, name) return object.__getattribute__(self, name)
except AttributeError:
if mine is not _MARKER: # First, look for descriptors in this object's type
return mine type_self = type(self)
raise descriptor = _WrapperType_Lookup(type_self, name)
if descriptor is _MARKER:
# Nothing in the class, go straight to the wrapped object
return getattr(_get_wrapped(self), name)
if hasattr(descriptor, '__get__'):
if not hasattr(descriptor, '__set__'):
# Non-data-descriptor: call through to the wrapped object
# to see if it's there
try:
return getattr(_get_wrapped(self), name)
except AttributeError:
pass
# Data-descriptor on this type. Call it
return descriptor.__get__(self, type_self)
return descriptor
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self._wrapped, name) return getattr(self._wrapped, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name == '_wrapped': if name == '_wrapped':
return super(PyProxyBase, self).__setattr__(name, value) return super(AbstractPyProxyBase, self).__setattr__(name, value)
try:
mine = super(PyProxyBase, self).__getattribute__(name) # First, look for descriptors in this object's type
except AttributeError: type_self = type(self)
descriptor = _WrapperType_Lookup(type_self, name)
if descriptor is _MARKER or not hasattr(descriptor, '__set__'):
# Nothing in the class that's a descriptor,
# go straight to the wrapped object
return setattr(self._wrapped, name, value) return setattr(self._wrapped, name, value)
else:
return object.__setattr__(self, name, value) return object.__setattr__(self, name, value)
def __delattr__(self, name): def __delattr__(self, name):
if name == '_wrapped': if name == '_wrapped':
...@@ -368,6 +422,12 @@ class PyProxyBase(object): ...@@ -368,6 +422,12 @@ class PyProxyBase(object):
self._wrapped = pow(self._wrapped, other, modulus) self._wrapped = pow(self._wrapped, other, modulus)
return self return self
class PyProxyBase(AbstractPyProxyBase):
"""Reference implementation.
"""
__slots__ = ('_wrapped', )
def py_getProxiedObject(obj): def py_getProxiedObject(obj):
if isinstance(obj, PyProxyBase): if isinstance(obj, PyProxyBase):
return obj._wrapped return obj._wrapped
...@@ -417,11 +477,19 @@ def py_removeAllProxies(obj): ...@@ -417,11 +477,19 @@ def py_removeAllProxies(obj):
obj = obj._wrapped obj = obj._wrapped
return obj return obj
_c_available = False
if 'PURE_PYTHON' not in os.environ:
try:
from zope.proxy._zope_proxy_proxy import ProxyBase as _c_available
except ImportError: #pragma NO COVER
pass
class PyNonOverridable(object): class PyNonOverridable(object):
"Deprecated, only for BWC."
def __init__(self, method_desc): #pragma NO COVER PyPy def __init__(self, method_desc): #pragma NO COVER PyPy
self.desc = method_desc self.desc = method_desc
try: if _c_available:
# Python API: not used in this module # Python API: not used in this module
from zope.proxy._zope_proxy_proxy import ProxyBase from zope.proxy._zope_proxy_proxy import ProxyBase
from zope.proxy._zope_proxy_proxy import getProxiedObject from zope.proxy._zope_proxy_proxy import getProxiedObject
...@@ -434,7 +502,8 @@ try: ...@@ -434,7 +502,8 @@ try:
# API for proxy-using C extensions. # API for proxy-using C extensions.
from zope.proxy._zope_proxy_proxy import _CAPI from zope.proxy._zope_proxy_proxy import _CAPI
except ImportError: #pragma NO COVER
else: #pragma NO COVER
# no C extension available, fall back # no C extension available, fall back
ProxyBase = PyProxyBase ProxyBase = PyProxyBase
getProxiedObject = py_getProxiedObject getProxiedObject = py_getProxiedObject
...@@ -444,7 +513,6 @@ except ImportError: #pragma NO COVER ...@@ -444,7 +513,6 @@ except ImportError: #pragma NO COVER
queryProxy = py_queryProxy queryProxy = py_queryProxy
queryInnerProxy = py_queryInnerProxy queryInnerProxy = py_queryInnerProxy
removeAllProxies = py_removeAllProxies removeAllProxies = py_removeAllProxies
non_overridable = PyNonOverridable
else: def non_overridable(func):
def non_overridable(func): return property(lambda self: func.__get__(self))
return property(lambda self: func.__get__(self))
...@@ -135,6 +135,30 @@ class SpecificationDecoratorBaseTests(unittest.TestCase): ...@@ -135,6 +135,30 @@ class SpecificationDecoratorBaseTests(unittest.TestCase):
proxy = self._makeOne(foo) proxy = self._makeOne(foo)
self.assertEqual(list(providedBy(proxy)), list(providedBy(foo))) self.assertEqual(list(providedBy(proxy)), list(providedBy(foo)))
def test_proxy_that_provides_interface_as_well_as_wrapped(self):
# If both the wrapper and the wrapped object provide
# interfaces, the wrapper provides the sum
from zope.interface import Interface
from zope.interface import implementer
from zope.interface import providedBy
class IFoo(Interface):
pass
@implementer(IFoo)
class Foo(object):
from_foo = 1
class IWrapper(Interface):
pass
@implementer(IWrapper)
class Proxy(self._getTargetClass()):
pass
foo = Foo()
proxy = Proxy(foo)
self.assertEqual(proxy.from_foo, 1)
self.assertEqual(list(providedBy(proxy)), [IFoo,IWrapper])
def test_suite(): def test_suite():
return unittest.TestSuite(( return unittest.TestSuite((
......
...@@ -418,7 +418,7 @@ class PyProxyBaseTestCase(unittest.TestCase): ...@@ -418,7 +418,7 @@ class PyProxyBaseTestCase(unittest.TestCase):
"complex(x)", "complex(x)",
] ]
if not PY3: # long is gone in Python 3 if not PY3: # long is gone in Python 3
ops.append("long(x)") ops.append("long(x)")
return ops return ops
def test_unops(self): def test_unops(self):
...@@ -581,6 +581,90 @@ class PyProxyBaseTestCase(unittest.TestCase): ...@@ -581,6 +581,90 @@ class PyProxyBaseTestCase(unittest.TestCase):
w = self._makeOne(o) w = self._makeOne(o)
self.assertTrue(w.__class__ is o.__class__) self.assertTrue(w.__class__ is o.__class__)
def test_descriptor__set___only_in_proxy_subclass(self):
class Descriptor(object):
value = None
instance = None
def __set__(self, instance, value):
self.value = value
self.instance = instance
descriptor = Descriptor()
class Proxy(self._getTargetClass()):
attr = descriptor
proxy = Proxy(object())
proxy.attr = 42
self.assertEqual(proxy.attr, descriptor)
self.assertEqual(descriptor.value, 42)
self.assertEqual(descriptor.instance, proxy)
def test_descriptor__get___set___in_proxy_subclass(self):
class Descriptor(object):
value = None
instance = None
cls = None
def __get__(self, instance, cls):
self.cls = cls
return self.value
def __set__(self, instance, value):
self.value = value
self.instance = instance
descriptor = Descriptor()
descriptor.value = "descriptor value"
class Proxy(self._getTargetClass()):
attr = descriptor
proxy = Proxy(object())
self.assertEqual(proxy.attr, "descriptor value")
self.assertEqual(descriptor.cls, Proxy)
proxy.attr = 42
self.assertEqual(descriptor.value, 42)
self.assertEqual(descriptor.instance, proxy)
def test_non_descriptor_in_proxy_subclass__dict__(self):
# Non-descriptors in the class dict of the subclass
# are always passed through to the wrapped instance
class Proxy(self._getTargetClass()):
attr = "constant value"
proxy = Proxy(object())
self.assertEqual(proxy.attr, "constant value")
self.assertRaises(AttributeError, setattr, proxy, 'attr', 42)
self.assertEqual(proxy.attr, "constant value")
def test_method_in_proxy_subclass(self):
class Proxy(self._getTargetClass()):
def __getitem__(self, k):
return k
proxy = Proxy(object())
# Both when called by the interpreter, which bypasses
# __getattribute__
self.assertEqual(proxy[42], 42)
# And when asked for as an attribute
self.assertNotEqual(getattr(proxy, '__getitem__'), self)
def test_string_to_int(self):
# XXX Implementation difference: This works in the
# Pure-Python version, but fails in CPython.
# See https://github.com/zopefoundation/zope.proxy/issues/4
proxy = self._makeOne("14")
try:
self.assertEqual(14, int(proxy))
except TypeError:
from zope.proxy import PyProxyBase
self.assertNotEqual(self._getTargetClass, PyProxyBase)
class ProxyBaseTestCase(PyProxyBaseTestCase): class ProxyBaseTestCase(PyProxyBaseTestCase):
...@@ -588,7 +672,6 @@ class ProxyBaseTestCase(PyProxyBaseTestCase): ...@@ -588,7 +672,6 @@ class ProxyBaseTestCase(PyProxyBaseTestCase):
from zope.proxy import ProxyBase from zope.proxy import ProxyBase
return ProxyBase return ProxyBase
class Test_py_getProxiedObject(unittest.TestCase): class Test_py_getProxiedObject(unittest.TestCase):
def _callFUT(self, *args): def _callFUT(self, *args):
...@@ -620,7 +703,6 @@ class Test_py_getProxiedObject(unittest.TestCase): ...@@ -620,7 +703,6 @@ class Test_py_getProxiedObject(unittest.TestCase):
proxy2 = self._makeProxy(proxy) proxy2 = self._makeProxy(proxy)
self.assertTrue(self._callFUT(proxy2) is proxy) self.assertTrue(self._callFUT(proxy2) is proxy)
class Test_getProxiedObject(Test_py_getProxiedObject): class Test_getProxiedObject(Test_py_getProxiedObject):
def _callFUT(self, *args): def _callFUT(self, *args):
......
...@@ -27,7 +27,7 @@ commands = ...@@ -27,7 +27,7 @@ commands =
[testenv:coverage] [testenv:coverage]
basepython = basepython =
python2.6 python2.7
commands = commands =
# The installed version messes up nose's test discovery / coverage reporting # The installed version messes up nose's test discovery / coverage reporting
# So, we uninstall that from the environment, and then install the editable # So, we uninstall that from the environment, and then install the editable
......
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