Index: Doc/library/typetools.rst =================================================================== --- Doc/library/typetools.rst (revision 0) +++ Doc/library/typetools.rst (revision 0) @@ -0,0 +1,69 @@ +:mod:`typetools` --- Utilities for working with Python types +============================================================ + +.. module:: typetools + :synopsis: Utilities for working with Python types and classes +.. moduleauthor:: Nick Coghlan +.. sectionauthor:: Nick Coghlan + + +.. versionadded:: 2.6 + +This module provides utilities for working with and +defining Python type objects (instances of the builtin metaclass +:class:`type`). The utilities provided by this module may be +useful regardless of whether the type instances are defined in +Python code using a normal :keyword:`class` statement or as part +of an extension module. + +The :mod:`typetools` module currently defines a single utility class: + +.. class:: ProxyMixin(target) + + A proxy class that ensures all special method invocations which may + otherwise bypass the normal :meth:`__getattribute__` lookup + process are correctly delegated to the specified target object. + Normal attribute manipulation operations are also delegated to the + specified target object. + + All operations on a :class:`ProxyMixin` instance other than in-place + assignment operations return an unproxied result. Operations involving + multiple :class:`ProxyMixin` instances (e.g. addition) are permitted, + and endeavour to return the same result as would be calculated if the + proxy objects were not involved. + + In-place assignment operations return the same proxy object if the + target's in-place assignment operation returns the target object. If + a different object is returned from the target's in-place assignment + operation, it is wrapped in a new proxy instance and the new proxy + object returned. + + Custom proxy class implementations may inherit from this class + in order to automatically delegate all such special methods + that the custom proxy class does not need to provide special + handling for. To support more complex target resolution methods + (e.g. holding only a weak reference to the target, which then + needs to be checked for validity before each operation in a fashion + similar to that used by ``weakref.proxy``) it is possible to + replace the simple :attr:`target` attribute on the base class + with a :class:`property` descriptor in the subclass. + + This may still not be sufficient for more complex delegation + scenarios which need to deal more carefully with the arguments + and return values of method calls (e.g. a local interface + to arbitrary remote objects), but :class:`ProxyMixin` and its test + cases may still be used as a reference when developing such + classes to identify which methods must be defined explicitly on + the proxy type in order for them to be delegated correctly by + the Python interpreter. + + ,, attr:: target + Override this attribute with a property descriptor in order to + alter how the proxy object acquires a reference to the target + object when performing operations. + + + .. method:: return_inplace(self, target, result) + Override this method to alter the way return values from + in-place assignment operations are handled (e.g. to + always return an unproxied result like ``weakref.proxy``) Property changes on: Doc/library/typetools.rst ___________________________________________________________________ Name: svn:eol-style + native Index: Lib/typetools.py =================================================================== --- Lib/typetools.py (revision 0) +++ Lib/typetools.py (revision 0) @@ -0,0 +1,217 @@ +"""typetools.py - Tools for working with Python type objects +""" +# Written by Nick Coghlan +# Copyright (C) 2008 Python Software Foundation. +from operator import (truediv, iadd, iand, idiv, ifloordiv, ilshift, imod, + imul, index, ior, ipow, irshift, isub, itruediv, ixor) +__all__ = ['ProxyMixin'] + +# Convenience functions to unwrap proxied objects +_PROXY_TARGET_ATTR = 'target' +def _deref(obj): + return object.__getattribute__(obj, _PROXY_TARGET_ATTR) + +def _unwrap(obj): + if isinstance(obj, ProxyMixin): + return _deref(obj) + return obj + +class ProxyMixin(object): + """Mixin class to delegate method calls that bypass __getattribute__ + + For optimisation purposes, Python implementations are permitted + to bypass the standard attribute lookup machinery for some + special methods. The precise list of methods which need special + treatment will be implementation specific. Inheriting from + this base class will ensure that a proxy implementation will + correctly delegate all of these special methods as needed.""" + + # Note that all non in-place operations return unwrapped objects + # Binary and ternary operations automatically unwrap other ProxyBase instances + # This behaviour follows the precedent set by weakref.proxy + + __slots__ = [_PROXY_TARGET_ATTR, '__weakref__'] + + def __init__(self, target): + object.__setattr__(self, _PROXY_TARGET_ATTR, target) + + # String representations + def __repr__(self): + return "<%s for %r>" % (type(self).__name__, _deref(self)) + def __str__(self): + return str(_deref(self)) + def __unicode__(self): + return unicode(_deref(self)) + def __oct__(self): + return oct(_deref(self)) + def __hex__(self): + return hex(_deref(self)) + def __format__(self, fmt): + return format(_deref(self), _unwrap(fmt)) + + # Equality and ordering checks + def __nonzero__(self): + return bool(_deref(self)) + def __hash__(self): + return hash(_deref(self)) + def __lt__(self, other): + return _deref(self) < _unwrap(other) + def __le__(self, other): + return _deref(self) <= _unwrap(other) + def __eq__(self, other): + return _deref(self) == _unwrap(other) + def __ne__(self, other): + return _deref(self) != _unwrap(other) + def __gt__(self, other): + return _deref(self) > _unwrap(other) + def __ge__(self, other): + return _deref(self) >= _unwrap(other) + def __cmp__(self, other): + return cmp(_deref(self), _unwrap(other)) + + # Callables + def __call__(self, *args, **kwds): + return _deref(self)(*args, **kwds) + + # Attribute access and manipulation + def __getattribute__(self, attr): + return getattr(_deref(self), attr) + def __setattr__(self, attr, value): + setattr(_deref(self), attr, value) + def __delattr__(self, attr): + delattr(_deref(self), attr) + + # Descriptors can't be correctly proxied by a value-based proxy, + # so the associated special methods __get__, __set__ and __delete__ + # are not implemented by this class + + # Container behaviour + def __len__(self): + return len(_deref(self)) + def __getitem__(self, key): + return _deref(self)[_unwrap(key)] + def __setitem__(self, key, value): + _deref(self)[_unwrap(key)] = _unwrap(value) + def __delitem__(self, key): + del _deref(self)[_unwrap(key)] + def __iter__(self): + return iter(_deref(self)) + def __contains__(self, item): + return _unwrap(item) in _deref(self) + + # Binary arithmetic operations with this instance on the left + def __add__(self, other): + return _deref(self) + _unwrap(other) + def __sub__(self, other): + return _deref(self) - _unwrap(other) + def __mul__(self, other): + return _deref(self) * _unwrap(other) + def __div__(self, other): + return _deref(self) / _unwrap(other) + def __truediv__(self, other): + return truediv(_deref(self), _unwrap(other)) + def __floordiv__(self, other): + return _deref(self) // _unwrap(other) + def __mod__(self, other): + return _deref(self) % _unwrap(other) + def __divmod__(self, other): + return divmod(_deref(self), _unwrap(other)) + def __pow__(self, other, modulo=None): + return pow(_deref(self), _unwrap(other), _unwrap(modulo)) + def __lshift__(self, other): + return _deref(self) << _unwrap(other) + def __rshift__(self, other): + return _deref(self) >> _unwrap(other) + def __and__(self, other): + return _deref(self) & _unwrap(other) + def __xor__(self, other): + return _deref(self) ^ _unwrap(other) + def __or__(self, other): + return _deref(self) | _unwrap(other) + + # Binary arithmetic operations with this instance on the right + def __radd__(self, other): + return _unwrap(other) + _deref(self) + def __rsub__(self, other): + return _unwrap(other) - _deref(self) + def __rmul__(self, other): + return _unwrap(other) * _deref(self) + def __rdiv__(self, other): + return _unwrap(other) / _deref(self) + def __rtruediv__(self, other): + return truediv(_unwrap(other), _deref(self)) + def __rfloordiv__(self, other): + return _unwrap(other) // _deref(self) + def __rmod__(self, other): + return _unwrap(other) % _deref(self) + def __rdivmod__(self, other): + return divmod(_unwrap(other), _deref(self)) + def __rpow__(self, other): + return _unwrap(other) ** _deref(self) + def __rlshift__(self, other): + return _unwrap(other) << _deref(self) + def __rrshift__(self, other): + return _unwrap(other) >> _deref(self) + def __rand__(self, other): + return _unwrap(other) & _deref(self) + def __rxor__(self, other): + return _unwrap(other) ^ _deref(self) + def __ror__(self, other): + return _unwrap(other) | _deref(self) + + # Allow subclasses to override in-place return behaviour + # (e.g. a ProxyMixin based implementation of weakref.proxy + # would want to strip the proxy for immutable targets to + # ensure there is a strong reference to the return value) + def return_inplace(self, target, result): + """Determine return value for an inplace operation given the current + target of the proxy and the result of the inplace operation on the + target.""" + if result is target: + return self + else: + return type(self)(result) + + # In-place assignment operations + def _make_inplace_proxy(op): + def inplace_proxy(self, other): + target = _deref(self) + result = op(target, _unwrap(other)) + return type(self).return_inplace(self, target, result) + return inplace_proxy + __iadd__ = _make_inplace_proxy(iadd) + __isub__ = _make_inplace_proxy(isub) + __imul__ = _make_inplace_proxy(imul) + __idiv__ = _make_inplace_proxy(idiv) + __itruediv__ = _make_inplace_proxy(itruediv) + __ifloordiv__ = _make_inplace_proxy(ifloordiv) + __imod__ = _make_inplace_proxy(imod) + __ipow__ = _make_inplace_proxy(ipow) + __ilshift__ = _make_inplace_proxy(ilshift) + __irshift__ = _make_inplace_proxy(irshift) + __iand__ = _make_inplace_proxy(iand) + __ixor__ = _make_inplace_proxy(ixor) + __ior__ = _make_inplace_proxy(ior) + del _make_inplace_proxy + + # Unary arithmetic operations + def __neg__(self): + return -_deref(self) + def __pos__(self): + return +_deref(self) + def __abs__(self): + return abs(_deref(self)) + def __invert__(self): + return ~_deref(self) + + # Numeric type coercion + def __complex__(self): + return complex(_deref(self)) + def __int__(self): + return int(_deref(self)) + def __long__(self): + return long(_deref(self)) + def __float__(self): + return float(_deref(self)) + def __index__(self): + return index(_deref(self)) Property changes on: Lib/typetools.py ___________________________________________________________________ Name: svn:keywords + Id Name: svn:eol-style + native Index: Lib/test/test_typetools.py =================================================================== --- Lib/test/test_typetools.py (revision 0) +++ Lib/test/test_typetools.py (revision 0) @@ -0,0 +1,316 @@ +"""test_typetools.py - Unit tests for the typetools module +""" +# Written by Nick Coghlan +# Copyright (C) 2008 Python Software Foundation. +import unittest +from test import test_support +import operator as ops +import typetools + +COMPARISON_OPS = (ops.eq, ops.ge, ops.gt, ops.le, ops.lt, ops.ne) + +# Test delegation of operations + +class TestProxyMixin(unittest.TestCase): + """Test delegation of assorted operations by ProxyMixin""" + TEST_CLASS = typetools.ProxyMixin + + UNARY_OPERANDS = [1, 1.0, set('ab'), lambda:42] + UNARY_OPERATIONS = [str, unicode, hex, oct, bool, hash, ops.neg, + ops.pos, ops.abs, ops.invert, complex, int, float, + long, ops.index, ops.truth, format] + + BINARY_OPERANDS = [(42, 42), + (1, 2), + (1.0, 2.0), + (1.0, 2), + (1, 2.0), + ('ab', 'abcd'), + ('ab', 5), + (5, 'ab'), + (set('ab'), set('abcd'))] + BINARY_OPERATIONS = [ops.add, ops.and_, ops.div, ops.eq, + ops.floordiv, ops.ge, ops.gt, ops.le, + ops.lshift, ops.lt, ops.mod, ops.mul, + ops.ne, ops.neg, ops.not_, ops.or_, ops.pos, + ops.pow, ops.repeat, ops.rshift, ops.sub, + ops.truediv, ops.xor] + IN_PLACE_OPERATIONS = [ops.iadd, ops.iand, ops.iconcat, ops.idiv, + ops.ifloordiv, ops.ilshift, ops.imod, + ops.imul, ops.ior, ops.ipow, ops.irepeat, + ops.irshift, ops.isub, ops.itruediv, ops.ixor] + + def _check_unary_op(self, op): + for x in self.UNARY_OPERANDS: + try: + expected = op(x) + except: + pass + else: + msg_format = "Op: %s, Arg: %r, Got: %r Expected: %r" + px = self.TEST_CLASS(x) + actual = op(px) + self.assertEqual(actual, expected, + msg_format % (op.__name__, px, actual, expected)) + + def _check_binary_op(self, op): + # For binary operations, implicit + # unwrapping should work properly + # regardless of whether the proxy + # object is the left operand or + # the right operand + for x, y in self.BINARY_OPERANDS: + try: + expected = op(x, y) + except: + pass + else: + msg_format = "Op: %s, LHS: %r, RHS: %r, Got: %r, Expected: %r" + def _check(lhs, rhs): + actual = op(lhs, rhs) + self.assertEqual(actual, expected, + msg_format % (op.__name__, lhs, rhs, actual, expected)) + px = self.TEST_CLASS(x) + py = self.TEST_CLASS(y) + # Work around the explicit comparison TypeErrors + # raised by the 2.x set implementation + if type(x) is not set or op not in COMPARISON_OPS: + _check(x, py) + _check(px, y) + _check(px, py) + + def _check_in_place_op(self, op): + # For in place operations, target object + # must be a proxy object for implicit + # unwrapping to work properly + for x, y in self.BINARY_OPERANDS: + try: + expected = op(x, y) + except: + pass + else: + msg_format = "Op: %s, LHS: %r, RHS: %r, Got: %r, Expected: %r" + def _check(lhs, rhs): + actual = op(lhs, rhs) + self.assertEqual(actual, expected, + msg_format % (op.__name__, lhs, rhs, + actual, expected)) + if expected is x: + self.assert_(actual is lhs, + msg_format % (op.__name__, lhs, rhs, + actual, expected)) + else: + self.assert_(actual is not lhs, + msg_format % (op.__name__, lhs, rhs, + actual, expected)) + self.assert_(isinstance(actual, type(lhs)), + msg_format % (op.__name__, lhs, rhs, + actual, expected)) + px = self.TEST_CLASS(x) + py = self.TEST_CLASS(y) + _check(px, y) + _check(px, py) + + def test_repr(self): + for x in self.UNARY_OPERANDS: + px = self.TEST_CLASS(x) + self.assertNotEqual(repr(x), repr(px)) + self.assert_(repr(x) in repr(px)) + + def test_unary_ops(self): + for op in self.UNARY_OPERATIONS: + self._check_unary_op(op) + + def test_binary_ops(self): + for op in self.BINARY_OPERATIONS: + self._check_binary_op(op) + + def test_in_place_ops(self): + for op in self.IN_PLACE_OPERATIONS: + self._check_in_place_op(op) + + def test_ternary_pow(self): + # For ternary pow(), first object + # must be a proxy objext for implicit + # unwrapping to work properly + x, y, z = 1, 2, 3 + expected = pow(x, y, z) + msg_format = "Base: %r, Exp: %r, Mod: %r, Got: %r, Expected: %r" + def _check(base, exp, mod): + actual = pow(base, exp, mod) + self.assertEqual(actual, expected, + msg_format % (base, exp, mod, actual, expected)) + px = self.TEST_CLASS(x) + py = self.TEST_CLASS(y) + pz = self.TEST_CLASS(z) + _check(px, y, z) + _check(px, y, pz) + _check(px, py, z) + _check(px, py, pz) + + def test_call(self): + def f(): + return 'Hello world!' + pf = self.TEST_CLASS(f) + expected = f() + self.assertEqual(pf(), expected) + + def test_attributes(self): + class C(object): pass + x = C() + x.a = 1 + x.b = 2 + x.c = 3 + px = self.TEST_CLASS(x) + self.assert_(px.a is x.a) + px.d = 10 + self.assert_(px.d is x.d) + self.assert_(px.c is x.c) + del px.c + self.assertFalse(hasattr(x, 'c')) + self.assertFalse(hasattr(px, 'c')) + self.assert_(px.__class__ is x.__class__) + + def test_containment(self): + x = dict(a=1, b=2, c=3) + px = self.TEST_CLASS(x) + self.assertEqual(len(px), len(x)) + self.assert_(px['a'] is x['a']) + px['d'] = 10 + self.assert_(px['d'] is x['d']) + self.assert_('c' in px) + del px['c'] + self.assert_('c' not in x) + self.assert_('c' not in px) + self.assertEqual(sorted(px), sorted(x)) + self.assertEqual(sorted(px.items()), sorted(x.items())) + + +# Test simple subclassing of ProxyMixin +class ProxyMixinSubclass(typetools.ProxyMixin): + pass + +class TestProxyMixinSubclass(TestProxyMixin): + """Ensure proxy operations still work for a subclass""" + TEST_CLASS = ProxyMixinSubclass + + +# Test use of a descriptor for more complex target resolution +class ProxyMixinDescriptor(typetools.ProxyMixin): + def _make_target_property(): + def pget(self): + """Hidden reference to target object""" + try: + return object.__getattribute__(self, "_descr_target") + except AttributeError: + raise ReferenceError("Proxy reference not set") + def pset(self, value): + object.__setattr__(self, "_descr_target", value) + def pdel(self): + object.__delattr__(self, "_descr_target") + return property(pget, pset, pdel) + target = _make_target_property() + del _make_target_property + +class TestProxyMixinDescriptor(TestProxyMixin): + """Ensure proxy operations still work with a descriptor for _target""" + TEST_CLASS = ProxyMixinDescriptor + + def test_descriptor(self): + # Check the descriptor is being invoked correctly + x = 1 + px = self.TEST_CLASS(x) + self.assertRaises(AttributeError, getattr, px, typetools._PROXY_TARGET_ATTR) + object.__delattr__(px, typetools._PROXY_TARGET_ATTR) + self.assertRaises(ReferenceError, getattr, px, typetools._PROXY_TARGET_ATTR) + + +# Test use of the override the result of inplace return methods +class ProxyMixinInplaceReturn(typetools.ProxyMixin): + def return_inplace(self, target, result): + return result + +class TestProxyMixinInplaceReturn(TestProxyMixin): + """Ensure return_inplace operates as documented""" + TEST_CLASS = ProxyMixinInplaceReturn + + def _check_in_place_op(self, op): + # For in place operations, target object + # must be a proxy object for implicit + # unwrapping to work properly + for x, y in self.BINARY_OPERANDS: + try: + expected = op(x, y) + except: + pass + else: + msg_format = "Op: %s, LHS: %r, RHS: %r, Got: %r, Expected: %r" + def _check(lhs, rhs): + actual = op(lhs, rhs) + self.assertEqual(actual, expected, + msg_format % (op.__name__, lhs, rhs, + actual, expected)) + if expected is x: + self.assert_(actual is expected, + msg_format % (op.__name__, lhs, rhs, + actual, expected)) + else: + self.assert_(type(actual) == type(expected), + msg_format % (op.__name__, lhs, rhs, + actual, expected)) + px = self.TEST_CLASS(x) + py = self.TEST_CLASS(y) + _check(px, y) + _check(px, py) + +class TestProxyMixinCoverage(unittest.TestCase): + """Ensure all operations in operator module are either + covered explicitly or deliberately skipped""" + + SKIPPED_OPERATIONS =[ops.attrgetter, ops.concat, ops.contains, + ops.countOf, ops.delitem, ops.delslice, + ops.getitem, ops.getslice, ops.indexOf, + ops.inv, ops.isCallable, ops.isMappingType, + ops.isNumberType, ops.isSequenceType, + ops.is_, ops.is_not, ops.itemgetter, + ops.methodcaller, ops.not_, + ops.sequenceIncludes, + ops.setitem, ops.setslice] + + + def test_ops_coverage(self): + tested_ops = set(TestProxyMixin.UNARY_OPERATIONS) + tested_ops |= set(TestProxyMixin.BINARY_OPERATIONS) + tested_ops |= set(TestProxyMixin.IN_PLACE_OPERATIONS) + skipped_ops = set(self.SKIPPED_OPERATIONS) + operator_ops = set(y for x, y in ops.__dict__.items() + if not x.startswith('__')) + uncovered_ops = operator_ops - tested_ops - skipped_ops + msg_format = "Uncovered operations in operator module: %s" + self.assertFalse(uncovered_ops, + msg_format % sorted(f.__name__ for f in uncovered_ops)) + + +def test_main(verbose=None): + import sys + test_classes = ( + TestProxyMixin, + TestProxyMixinSubclass, + TestProxyMixinDescriptor, + TestProxyMixinInplaceReturn, + TestProxyMixinCoverage, + ) + test_support.run_unittest(*test_classes) + + # verify reference counting + if verbose and hasattr(sys, "gettotalrefcount"): + import gc + counts = [None] * 5 + for i in xrange(len(counts)): + test_support.run_unittest(*test_classes) + gc.collect() + counts[i] = sys.gettotalrefcount() + print counts + +if __name__ == '__main__': + test_main(verbose=True) Property changes on: Lib/test/test_typetools.py ___________________________________________________________________ Name: svn:keywords + Id Name: svn:eol-style + native