Skip to content

Commit

Permalink
Handle descriptors defined in PyProxyBase subclasses the same way the…
Browse files Browse the repository at this point in the history
… C version does. This fixes #5.
  • Loading branch information
jamadden committed May 7, 2015
1 parent 65ebb9d commit fd0e8f7
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 30 deletions.
4 changes: 3 additions & 1 deletion CHANGES.rst
Expand Up @@ -4,7 +4,9 @@ Changes
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)
------------------
Expand Down
87 changes: 61 additions & 26 deletions src/zope/proxy/__init__.py
Expand Up @@ -14,6 +14,7 @@
"""More convenience functions for dealing with proxies.
"""
import operator
import os
import pickle
import sys

Expand All @@ -33,6 +34,22 @@ def ProxyIterator(p):

_MARKER = object()

def _WrapperType_Lookup(type_, name):
"""
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 PyProxyBase:
continue
res = base.__dict__.get(name, _MARKER)
if res is not _MARKER:
return res
return _MARKER

class PyProxyBase(object):
"""Reference implementation.
"""
Expand Down Expand Up @@ -95,32 +112,47 @@ def __getattribute__(self, name):
wrapped = super(PyProxyBase, self).__getattribute__('_wrapped')
if name == '_wrapped':
return wrapped
try:
mine = super(PyProxyBase, self).__getattribute__(name)
except AttributeError:
mine = _MARKER
else:
if isinstance(mine, PyNonOverridable): #pragma NO COVER PyPy
return mine.desc.__get__(self)
try:

if name == '__class__':
# __class__ is special cased in the C implementation
return wrapped.__class__

# First, look for descriptors in this object's type
type_self = type(self)
descriptor = _WrapperType_Lookup(type_self, name)
if descriptor is _MARKER:
# Nothing in the class, go straight to the wrapped object
return getattr(wrapped, name)
except AttributeError:
if mine is not _MARKER:
return mine
raise


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(wrapped, name)
except AttributeError:
raise
# Data-descriptor on this type. Call it
return descriptor.__get__(self, type_self)
return descriptor

def __getattr__(self, name):
return getattr(self._wrapped, name)

def __setattr__(self, name, value):
if name == '_wrapped':
return super(PyProxyBase, self).__setattr__(name, value)
try:
mine = super(PyProxyBase, self).__getattribute__(name)
except AttributeError:

# First, look for descriptors in this object's type
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)
else:
return object.__setattr__(self, name, value)

return object.__setattr__(self, name, value)

def __delattr__(self, name):
if name == '_wrapped':
Expand Down Expand Up @@ -417,11 +449,14 @@ def py_removeAllProxies(obj):
obj = obj._wrapped
return obj

class PyNonOverridable(object):
def __init__(self, method_desc): #pragma NO COVER PyPy
self.desc = method_desc
_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

try:
if _c_available:
# Python API: not used in this module
from zope.proxy._zope_proxy_proxy import ProxyBase
from zope.proxy._zope_proxy_proxy import getProxiedObject
Expand All @@ -434,7 +469,8 @@ def __init__(self, method_desc): #pragma NO COVER PyPy

# API for proxy-using C extensions.
from zope.proxy._zope_proxy_proxy import _CAPI
except ImportError: #pragma NO COVER

else: #pragma NO COVER
# no C extension available, fall back
ProxyBase = PyProxyBase
getProxiedObject = py_getProxiedObject
Expand All @@ -444,7 +480,6 @@ def __init__(self, method_desc): #pragma NO COVER PyPy
queryProxy = py_queryProxy
queryInnerProxy = py_queryInnerProxy
removeAllProxies = py_removeAllProxies
non_overridable = PyNonOverridable
else:
def non_overridable(func):
return property(lambda self: func.__get__(self))

def non_overridable(func):
return property(lambda self: func.__get__(self))
24 changes: 24 additions & 0 deletions src/zope/proxy/tests/test_decorator.py
Expand Up @@ -135,6 +135,30 @@ class Foo(object):
proxy = self._makeOne(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():
return unittest.TestSuite((
Expand Down
76 changes: 73 additions & 3 deletions src/zope/proxy/tests/test_proxy.py
Expand Up @@ -418,7 +418,7 @@ def unops(self):
"complex(x)",
]
if not PY3: # long is gone in Python 3
ops.append("long(x)")
ops.append("long(x)")
return ops

def test_unops(self):
Expand Down Expand Up @@ -581,14 +581,85 @@ def test___class__(self):
w = self._makeOne(o)
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_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):

def _getTargetClass(self):
from zope.proxy import ProxyBase
return ProxyBase


class Test_py_getProxiedObject(unittest.TestCase):

def _callFUT(self, *args):
Expand Down Expand Up @@ -620,7 +691,6 @@ class C(object):
proxy2 = self._makeProxy(proxy)
self.assertTrue(self._callFUT(proxy2) is proxy)


class Test_getProxiedObject(Test_py_getProxiedObject):

def _callFUT(self, *args):
Expand Down

0 comments on commit fd0e8f7

Please sign in to comment.