Commit 4053eadd authored by Jason Madden's avatar Jason Madden

Add tests for all the methods proxied by the C wrappers, and make the Python...

Add tests for all the methods proxied by the C wrappers, and make the Python implementation match that behaviour.
parent 5438ceaf
......@@ -3,6 +3,7 @@ from __future__ import absolute_import, print_function
# pylint:disable=W0212,R0911,R0912
import os
import operator
import sys
import types
......@@ -12,8 +13,6 @@ from zope.interface import classImplements
from .interfaces import IAcquirer
from .interfaces import IAcquisitionWrapper
from ._proxy import PyProxyBase
class Acquired(object):
"Marker for explicit acquisition"
......@@ -212,7 +211,9 @@ def _Wrapper_findattr(wrapper, name,
if predicate:
return result if _apply_filter(predicate, wrapper, orig_name, result, predicate_extra, orig_object) else None
return result
elif name in ('__reduce__', '__reduce_ex__', '__getstate__', '__of__'):
elif name in ('__reduce__', '__reduce_ex__', '__getstate__',
'__of__', '__cmp__', '__eq__', '__ne__', '__lt__',
'__le__', '__gt__', '__ge__'):
return object.__getattribute__(wrapper, orig_name)
# If we're doing a containment search, replace the wrapper with aq_inner
......@@ -282,17 +283,18 @@ def _Wrapper_findattr(wrapper, name,
_NOT_GIVEN = object() # marker
class _Wrapper(PyProxyBase):
__slots__ = ('_container',)
class _Wrapper(ExtensionClass.Base):
__slots__ = ('_obj','_container',)
_IS_IMPLICIT = None
def __new__(cls, obj, container):
inst = PyProxyBase.__new__(cls, obj)
inst = super(_Wrapper,cls).__new__(cls)
inst._obj = obj
inst._container = container
return inst
def __init__(self, obj, container):
PyProxyBase.__init__(self, obj)
super(_Wrapper,self).__init__()
self._obj = obj
self._container = container
......@@ -382,8 +384,76 @@ class _Wrapper(PyProxyBase):
def __getnewargs__(self):
return ()
# Methods looked up by the type of self._obj
# NOTE: This is probably incomplete
# Equality and comparisons
def __hash__(self):
# The C implementation doesn't pass the wrapper
# to any __hash__ that the object implements,
# so it can't access derived attributes.
# (If that changes, just add this to __unary_special_methods__
# and remove this method)
return hash(self._obj)
# The C implementation forces all comparisons through the
# __cmp__ method, if it's implemented. If it's not implemented,
# then comparisons are based strictly on the memory addresses
# of the underlying object (aq_base). We could mostly emulate this behaviour
# on Python 2, but on Python 3 __cmp__ is gone, so users won't
# have an expectation to write it.
# Because users have never had an expectation that the rich comparison
# methods would be called on their wrapped objects (and so would not be
# accessing acquired attributes there), we can't/don't want to start
# proxying to them?
# For the moment, we settle for an emulation of the C behaviour:
# define __cmp__ the same way, and redirect the rich comparison operators
# to it. (Note that these attributes are also hardcoded in getattribute)
def __cmp__(self, other):
aq_self = self._obj
if hasattr(type(aq_self), '__cmp__'):
return _rebound_method(type(aq_self), self)(other)
my_base = aq_base(self)
other_base = aq_base(other)
if my_base is other_base:
return 0
return -1 if id(my_base) < id(other_base) else 1
def __eq__(self, other):
return self.__cmp__(other) == 0
def __ne__(self, other):
return self.__cmp__(other) != 0
def __lt__(self, other):
return self.__cmp__(other) < 0
def __le__(self, other):
return self.__cmp__(other) <= 0
def __gt__(self, other):
return self.__cmp__(other) > 0
def __ge__(self, other):
return self.__cmp__(other) >= 0
# Special methods looked up by the type of self._obj,
# but which must have the wrapper as self when called
def __nonzero__(self):
aq_self = self._obj
type_aq_self = type(aq_self)
nonzero = getattr(type_aq_self, '__nonzero__', None)
if nonzero is None:
# Py3 bool?
nonzero = getattr(type_aq_self, '__bool__', None)
if nonzero is None:
# a len?
nonzero = getattr(type_aq_self, '__len__', None)
if nonzero:
return _rebound_method(nonzero, self)()
# If nothing was defined, then it's true
return True
def __unicode__(self):
f = getattr(self.aq_self, '__unicode__',
......@@ -398,6 +468,107 @@ class _Wrapper(PyProxyBase):
aq_self = self._obj
return type(aq_self).__str__(aq_self)
__binary_special_methods__ = [
# general numeric
'__add__',
'__sub__',
'__mul__',
'__floordiv__', # not implemented in C
'__mod__',
'__divmod__',
'__pow__',
'__lshift__',
'__rshift__',
'__and__',
'__xor__',
'__or__',
# division; only one of these will be used at any one time
'__truediv__',
'__div__',
# reflected numeric
'__radd__',
'__rsub__',
'__rmul__',
'__rdiv__',
'__rtruediv__',
'__rfloordiv__',
'__rmod__',
'__rdivmod__',
'__rpow__',
'__rlshift__',
'__rrshift__',
'__rand__',
'__rxor__',
'__ror__',
# in place numeric
'__iadd__',
'__isub__',
'__imul__',
'__idiv__',
'__itruediv__',
'__ifloordiv__',
'__imod__',
'__idivmod__',
'__ipow__',
'__ilshift__',
'__irshift__',
'__iand__',
'__ixor__',
'__ior__',
# conversion
'__coerce__',
# container
'__delitem__',
]
__unary_special_methods__ = [
# arithmetic
'__neg__',
'__pos__',
'__abs__',
'__invert__',
# conversion
'__complex__',
'__int__',
'__long__',
'__float__',
'__oct__',
'__hex__',
'__index__',
'__len__',
# strings
#'__repr__',
#'__str__',
]
for _name in __binary_special_methods__:
def _make_op(_name):
def op(self, other):
aq_self = self._obj
return getattr(type(aq_self), _name)(self, other)
return op
locals()[_name] = _make_op(_name)
for _name in __unary_special_methods__:
def _make_op(_name):
def op(self):
aq_self = self._obj
return getattr(type(aq_self), _name)(self)
return op
locals()[_name] = _make_op(_name)
del _make_op
del _name
# Container protocol
def __iter__(self):
# For things that provide either __iter__ or just __getitem__,
# we need to be sure that the wrapper is provided as self
......@@ -421,6 +592,50 @@ class _Wrapper(PyProxyBase):
return iter(self._obj)
def __contains__(self, item):
# First, if the type of the object defines __contains__ then
# use it
aq_self = self._obj
aq_contains = getattr(type(aq_self), '__contains__', None)
if aq_contains:
return _rebound_method(aq_contains, self)(item)
# Next, we should attempt to iterate like the interpreter; but the C code doesn't
# do this, so we don't either.
#return item in iter(self)
raise AttributeError('__contains__')
def __setitem__(self, key, value):
aq_self = self._obj
_rebound_method(getattr(type(aq_self), '__setitem__'), self)(key, value)
def __getitem__(self, key):
if isinstance(key, slice):
if isinstance(self._obj, (list, tuple)):
return self._obj[key]
start, stop = key.start, key.stop
if start is None:
start = 0
if start < 0:
start += len(self._obj)
if stop is None:
stop = getattr(sys, 'maxint', None) # PY2
elif stop < 0:
stop += len(self._obj)
if hasattr(operator, 'getslice'): # PY2
return operator.getslice(self._obj, start, stop)
return self._obj[start:stop]
return self._obj[key]
def __call__(self, *args, **kwargs):
try:
call = getattr(type(self._obj), '__call__')
except AttributeError:
# A TypeError is what the interpreter raises;
# AttributeError is allowed to percolate through the
# C proxy
raise TypeError('object is not callable')
else:
return _rebound_method(call, self)(*args, **kwargs)
class ImplicitAcquisitionWrapper(_Wrapper):
_IS_IMPLICIT = True
......
##############################################################################
#
# Copyright (c) 2003 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (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.
#
##############################################################################
"""Taken from zope.proxy to support acquisition wrappers.
"""
import operator
import pickle
import sys
import ExtensionClass
_MARKER = object()
class PyProxyBase(ExtensionClass.Base):
"""Reference implementation.
"""
__slots__ = ('_obj', )
def __new__(cls, value):
inst = super(PyProxyBase, cls).__new__(cls)
inst._obj = value
return inst
def __init__(self, obj):
self._obj = obj
def __call__(self, *args, **kw):
return self._obj(*args, **kw)
def __repr__(self):
return repr(self._obj)
def __str__(self):
return str(self._obj)
def __unicode__(self):
return unicode(self._obj)
def __reduce__(self): #pragma NO COVER (__reduce_ex__ prevents normal)
raise pickle.PicklingError
def __reduce_ex__(self, proto):
raise pickle.PicklingError
# Rich comparison protocol
def __lt__(self, other):
return self._obj < other
def __le__(self, other):
return self._obj <= other
def __eq__(self, other):
return self._obj == other
def __ne__(self, other):
return self._obj != other
def __gt__(self, other):
return self._obj > other
def __ge__(self, other):
return self._obj >= other
def __nonzero__(self):
return bool(self._obj)
__bool__ = __nonzero__ # Python3 compat
def __hash__(self):
return hash(self._obj)
# Attribute protocol
# Left for the _Wrapper subclass
# Container protocols
def __len__(self):
return len(self._obj)
def __getitem__(self, key):
if isinstance(key, slice):
if isinstance(self._obj, (list, tuple)):
return self._obj[key]
start, stop = key.start, key.stop
if start is None:
start = 0
if start < 0:
start += len(self._obj)
if stop is None:
stop = getattr(sys, 'maxint', None) # PY2
elif stop < 0:
stop += len(self._obj)
if hasattr(operator, 'setslice'): # PY2
return operator.getslice(self._obj, start, stop)
return self._obj[start:stop]
return self._obj[key]
def __setitem__(self, key, value):
self._obj[key] = value
def __delitem__(self, key):
del self._obj[key]
def __iter__(self):
# This handles a custom __iter__ and generator support at the same time.
return iter(self._obj)
def next(self):
# Called when we wrap an iterator itself.
return self._obj.next()
def __next__(self): #pragma NO COVER Python3
return self._obj.__next__()
# Python 2.7 won't let the C wrapper support __reversed__ :(
#def __reversed__(self):
# return reversed(self._obj)
def __contains__(self, item):
return item in self._obj
# Numeric protocol: unary operators
def __neg__(self):
return -self._obj
def __pos__(self):
return +self._obj
def __abs__(self):
return abs(self._obj)
def __invert__(self):
return ~self._obj
# Numeric protocol: unary conversions
def __complex__(self):
return complex(self._obj)
def __int__(self):
return int(self._obj)
def __long__(self):
return long(self._obj)
def __float__(self):
return float(self._obj)
def __oct__(self):
return oct(self._obj)
def __hex__(self):
return hex(self._obj)
def __index__(self):
return operator.index(self._obj)
# Numeric protocol: binary coercion
def __coerce__(self, other):
left, right = coerce(self._obj, other)
if left == self._obj and type(left) is type(self._obj):
left = self
return left, right
# Numeric protocol: binary arithmetic operators
def __add__(self, other):
return self._obj + other
def __sub__(self, other):
return self._obj - other
def __mul__(self, other):
return self._obj * other
def __floordiv__(self, other):
return self._obj // other
def __truediv__(self, other): #pragma NO COVER
# Only one of __truediv__ and __div__ is meaningful at any one time.
return self._obj / other
def __div__(self, other): #pragma NO COVER
# Only one of __truediv__ and __div__ is meaningful at any one time.
return self._obj / other
def __mod__(self, other):
return self._obj % other
def __divmod__(self, other):
return divmod(self._obj, other)
def __pow__(self, other, modulus=None):
if modulus is None:
return pow(self._obj, other)
return pow(self._obj, other, modulus)
def __radd__(self, other):
return other + self._obj
def __rsub__(self, other):
return other - self._obj
def __rmul__(self, other):
return other * self._obj
def __rfloordiv__(self, other):
return other // self._obj
def __rtruediv__(self, other): #pragma NO COVER
# Only one of __rtruediv__ and __rdiv__ is meaningful at any one time.
return other / self._obj
def __rdiv__(self, other): #pragma NO COVER
# Only one of __rtruediv__ and __rdiv__ is meaningful at any one time.
return other / self._obj
def __rmod__(self, other):
return other % self._obj
def __rdivmod__(self, other):
return divmod(other, self._obj)
def __rpow__(self, other, modulus=None):
if modulus is None:
return pow(other, self._obj)
# We can't actually get here, because we can't lie about our type()
return pow(other, self._obj, modulus) #pragma NO COVER
# Numeric protocol: binary bitwise operators
def __lshift__(self, other):
return self._obj << other
def __rshift__(self, other):
return self._obj >> other
def __and__(self, other):
return self._obj & other
def __xor__(self, other):
return self._obj ^ other
def __or__(self, other):
return self._obj | other
def __rlshift__(self, other):
return other << self._obj
def __rrshift__(self, other):
return other >> self._obj
def __rand__(self, other):
return other & self._obj
def __rxor__(self, other):
return other ^ self._obj
def __ror__(self, other):
return other | self._obj
# Numeric protocol: binary in-place operators
def __iadd__(self, other):
self._obj += other
return self
def __isub__(self, other):
self._obj -= other
return self
def __imul__(self, other):
self._obj *= other
return self
def __idiv__(self, other): #pragma NO COVER
# Only one of __itruediv__ and __idiv__ is meaningful at any one time.
self._obj /= other
return self
def __itruediv__(self, other): #pragma NO COVER
# Only one of __itruediv__ and __idiv__ is meaningful at any one time.
self._obj /= other
return self
def __ifloordiv__(self, other):
self._obj //= other
return self
def __imod__(self, other):
self._obj %= other
return self
def __ilshift__(self, other):
self._obj <<= other
return self
def __irshift__(self, other):
self._obj >>= other
return self
def __iand__(self, other):
self._obj &= other
return self
def __ixor__(self, other):
self._obj ^= other
return self
def __ior__(self, other):
self._obj |= other
return self
def __ipow__(self, other, modulus=None):
if modulus is None:
self._obj **= other
else: #pragma NO COVER
# There is no syntax which triggers in-place pow w/ modulus
self._obj = pow(self._obj, other, modulus)
return self
......@@ -332,8 +332,11 @@ from __future__ import print_function
import gc
import unittest
import sys
import platform
import operator
from doctest import DocTestSuite, DocFileSuite
if sys.version_info >= (3,):
PY3 = True
PY2 = False
......@@ -348,6 +351,8 @@ if sys.version_info >= (3,):
else:
PY2 = True
PY3 = False
py_impl = getattr(platform, 'python_implementation', lambda: None)
PYPY = py_impl() == 'PyPy'
import ExtensionClass
import Acquisition
......@@ -1853,8 +1858,8 @@ if hasattr(gc, 'get_threshold'):
"""
def test_proxying():
"""Make sure that recent python slots are proxied.
def test_container_proxying():
"""Make sure that recent python container-related slots are proxied.
>>> import sys
>>> import Acquisition
......@@ -2065,7 +2070,6 @@ def test_proxying():
"""
class Location(object):
__parent__ = None
......@@ -2673,6 +2677,349 @@ class TestUnicode(unittest.TestCase):
inner = A().__of__(outer)
self.assertEqual(u'True', unicode(inner))
class TestProxying(unittest.TestCase):
__binary_numeric_methods__ = [
'__add__',
'__sub__',
'__mul__',
# '__floordiv__', # not implemented in C
'__mod__',
'__divmod__',
'__pow__',
'__lshift__',
'__rshift__',
'__and__',
'__xor__',
'__or__',
# division
'__truediv__',
'__div__',
# reflected
'__radd__',
'__rsub__',
'__rmul__',
'__rdiv__',
'__rtruediv__',
'__rfloordiv__',
'__rmod__',
'__rdivmod__',
'__rpow__',
'__rlshift__',
'__rrshift__',
'__rand__',
'__rxor__',
'__ror__',
# in place
'__iadd__',
'__isub__',
'__imul__',
'__idiv__',
'__itruediv__',
'__ifloordiv__',
'__imod__',
'__idivmod__',
'__ipow__',
'__ilshift__',
'__irshift__',
'__iand__',
'__ixor__',
'__ior__',
# conversion
# implementing it messes up all the arithmetic tests
#'__coerce__',
]
__unary_special_methods__ = [
# arithmetic
'__neg__',
'__pos__',
'__abs__',
'__invert__',
]
__unary_conversion_methods__ = {
# conversion
'__complex__': complex,
'__int__': int,
'__long__': long,
'__float__': float,
'__oct__': oct,
'__hex__': hex,
'__len__': lambda o: o if isinstance(o,int) else len(o),
#'__index__': operator.index, # not implemented in C
}
def _check_special_methods(self,base_class=Acquisition.Implicit):
"Check that special methods are proxied when called implicitly by the interpreter"
def binary_acquired_func(self, other):
return self.value
def unary_acquired_func(self):
return self.value
acquire_meths = {k: binary_acquired_func
for k in self.__binary_numeric_methods__}
acquire_meths.update( {k: unary_acquired_func
for k in self.__unary_special_methods__} )
def make_converter(f):
def converter(self,*args):
return f(self.value)
return converter
acquire_meths.update( {k: make_converter(convert)
for k, convert
in self.__unary_conversion_methods__.items()})
acquire_meths['__len__'] = lambda self: self.value
if base_class == Acquisition.Explicit:
acquire_meths['value'] = Acquisition.Acquired
AcquireValue = type('AcquireValue', (base_class,), acquire_meths)
class B(Acquisition.Implicit):
pass
base = B()
base.value = 42
base.derived = AcquireValue()
# one syntax check for the heck of it
self.assertEqual(base.value, base.derived + 1)
# divmod is not in the operator module
self.assertEqual(base.value, divmod(base.derived, 1))
_found_at_least_one_div = False
for meth in self.__binary_numeric_methods__:
# called on the instance
self.assertEqual(base.value,
getattr(base.derived, meth)(-1))
# called on the type, as the interpreter does
# Note that the C version can only implement either __truediv__
# or __div__, not both
op = getattr(operator, meth, None)
if op is not None:
try:
self.assertEqual(base.value,
op(base.derived, 1))
if meth in ('__div__', '__truediv__'):
_found_at_least_one_div = True
except TypeError:
if meth in ('__div__', '__truediv__'):
pass
self.assertTrue(_found_at_least_one_div, "Must implement at least one of __div__ and __truediv__")
# Unary methods
for meth in self.__unary_special_methods__:
self.assertEqual(base.value,
getattr(base.derived, meth)())
op = getattr(operator, meth)
self.assertEqual(base.value,
op(base.derived) )
# Conversion functions
for meth, converter in self.__unary_conversion_methods__.items():
if not converter:
continue
self.assertEqual(converter(base.value),
getattr(base.derived, meth)())
self.assertEqual(converter(base.value),
converter(base.derived))
def test_implicit_proxy_special_meths(self):
self._check_special_methods()
def test_explicit_proxy_special_meths(self):
self._check_special_methods(base_class=Acquisition.Explicit)
def _check_contains(self, base_class=Acquisition.Implicit):
# Contains has lots of fallback behaviour
class B(Acquisition.Implicit):
pass
base = B()
base.value = 42
# The simple case is if the object implements contains itself
class ReallyContains(base_class):
if base_class is Acquisition.Explicit:
value = Acquisition.Acquired
def __contains__(self, item):
return self.value == item
base.derived = ReallyContains()
self.assertTrue(42 in base.derived)
self.assertFalse(24 in base.derived)
# Iterable objects are NOT iterated
# XXX: Is this a bug in the C code? Shouldn't it do
# what the interpreter does and fallback to iteration?
class IterContains(base_class):
if base_class is Acquisition.Explicit:
value = Acquisition.Acquired
def __iter__(self):
return iter((42,))
base.derived = IterContains()
self.assertRaises(AttributeError, operator.contains, base.derived, 42)
def test_implicit_proxy_contains(self):
self._check_contains()
def test_explicit_proxy_contains(self):
self._check_contains(base_class=Acquisition.Explicit)
def _check_call(self, base_class=Acquisition.Implicit):
class B(Acquisition.Implicit):
pass
base = B()
base.value = 42
class Callable(base_class):
if base_class is Acquisition.Explicit:
value = Acquisition.Acquired
def __call__(self, arg, k=None):
return self.value, arg, k
base.derived = Callable()
self.assertEqual( base.derived(1, k=2),
(42, 1, 2))
if not PYPY:
# XXX: This test causes certain versions
# of PyPy to segfault (at least 2.6.0-alpha1)
class NotCallable(base_class):
pass
base.derived = NotCallable()
try:
base.derived()
self.fail("Not callable")
except (TypeError,AttributeError):
pass
def test_implicit_proxy_call(self):
self._check_call()
def test_explicit_proxy_call(self):
self._check_call(base_class=Acquisition.Explicit)
def _check_hash(self,base_class=Acquisition.Implicit):
class B(Acquisition.Implicit):
pass
base = B()
base.value = B()
base.value.hash = 42
class NoAcquired(base_class):
def __hash__(self):
return 1
hashable = NoAcquired()
base.derived = hashable
self.assertEqual(1, hash(hashable))
self.assertEqual(1, hash(base.derived))
# cannot access acquired attributes during
# __hash__
class CannotAccessAcquiredAttributesAtHash(base_class):
if base_class is Acquisition.Explicit:
value = Acquisition.Acquired
def __hash__(self):
return self.value.hash
hashable = CannotAccessAcquiredAttributesAtHash()
base.derived = hashable
self.assertRaises(AttributeError, hash, hashable)
self.assertRaises(AttributeError, hash, base.derived)
def test_implicit_proxy_hash(self):
self._check_hash()
def test_explicit_proxy_hash(self):
self._check_hash(base_class=Acquisition.Explicit)
def _check_comparison(self,base_class=Acquisition.Implicit):
# Comparison behaviour is complex; see notes in _Wrapper
class B(Acquisition.Implicit):
pass
base = B()
base.value = 42
rich_cmp_methods = ['__lt__', '__gt__', '__eq__',
'__ne__', '__ge__', '__le__']
def _never_called(self, other):
raise RuntimeError("This should never be called")
class RichCmpNeverCalled(base_class):
for _name in rich_cmp_methods:
locals()[_name] = _never_called
base.derived = RichCmpNeverCalled()
base.derived2 = RichCmpNeverCalled()
# We can access all of the operators, but only because
# they are masked
for name in rich_cmp_methods:
getattr(operator, name)(base.derived, base.derived2)
self.assertFalse(base.derived2 == base.derived)
self.assertEquals(base.derived, base.derived)
def test_implicit_proxy_comporison(self):
self._check_comparison()
def test_explicit_proxy_comporison(self):
self._check_comparison(base_class=Acquisition.Explicit)
def _check_bool(self, base_class=Acquisition.Implicit):
class B(Acquisition.Implicit):
pass
base = B()
base.value = 42
class WithBool(base_class):
if base_class is Acquisition.Explicit:
value = Acquisition.Acquired
def __nonzero__(self):
return bool(self.value)
class WithLen(base_class):
if base_class is Acquisition.Explicit:
value = Acquisition.Acquired
def __len__(self):
return self.value
class WithNothing(base_class):
pass
base.wbool = WithBool()
base.wlen = WithLen()
base.wnothing = WithNothing()
self.assertEqual(bool(base.wbool), True)
self.assertEqual(bool(base.wlen), True)
self.assertEqual(bool(base.wnothing), True)
base.value = 0
self.assertFalse(base.wbool)
self.assertFalse(base.wlen)
def test_implicit_proxy_bool(self):
self._check_bool()
def test_explicit_proxy_bool(self):
self._check_bool(base_class=Acquisition.Explicit)
def test_suite():
import os.path
......@@ -2685,6 +3032,7 @@ def test_suite():
unittest.makeSuite(TestParent),
unittest.makeSuite(TestAcquire),
unittest.makeSuite(TestUnicode),
unittest.makeSuite(TestProxying),
]
# 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