From af08c72a39d98cc1857da51ac7ec0360fd7569f0 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 6 Oct 2013 14:12:50 +1100 Subject: [PATCH] Object proxy documentation. --- docs/index.rst | 2 +- docs/wrappers.rst | 318 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index de1574f..9ce51a5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,11 +53,11 @@ Contents quick-start decorators + wrappers examples benchmarks testing changes issues -.. wrappers .. proxies diff --git a/docs/wrappers.rst b/docs/wrappers.rst index 8586a73..3772f65 100644 --- a/docs/wrappers.rst +++ b/docs/wrappers.rst @@ -1,2 +1,320 @@ Wrappers ======== + +Underlying the implementation of the decorators created by the **wrapt** +module is a wrapper class which acts as a transparent object proxy. This +document describes the object proxy and the various custom wrappers provided. + +Object Proxy +------------ + +The object proxy class is available as ``wrapt.ObjectProxy``. The class +would not normally be used directly, but as a base class to custom object +proxies or wrappers which add behaviour which overrides that of the +original object. When an object proxy is used, it will pass through any +actions performed on the proxy through to the wrapped object. + +:: + + >>> table = {} + >>> proxy = wrapt.ObjectProxy(table) + >>> proxy['key-1'] = 'value-1' + >>> proxy['key-2'] = 'value-2' + + >>> proxy.keys() + ['key-2', 'key-1'] + >>> table.keys() + ['key-2', 'key-1'] + + >>> isinstance(proxy, dict) + True + + >>> dir(proxy) + ['__class__', '__cmp__', '__contains__', '__delattr__', '__delitem__', + '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', + '__getitem__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', + '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', + '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', + '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'has_key', + 'items', 'iteritems', 'iterkeys', 'itervalues', 'keys', 'pop', + 'popitem', 'setdefault', 'update', 'values'] + +This ability for a proxy to stand in for the original goes as far as +arithmetic operations, rich comparison and hashing. + +:: + + >>> value = 1 + >>> proxy = wrapt.ObjectProxy(value) + + >>> proxy + 1 + 2 + + >>> int(proxy) + 1 + >>> hash(proxy) + 1 + >>> hash(value) + 1 + + >>> proxy < 2 + True + >>> proxy == 0 + False + +Do note however, that when wrapping an object proxy around a literal value, +the original value is effectively copied into the proxy object and any +operation which updates the value will only update the value held by the +proxy object. + +:: + + >>> value = 1 + >>> proxy = wrapt.ObjectProxy(value) + >>> type(proxy) + + + >>> proxy += 1 + + >>> type(proxy) + + + >>> print(proxy) + 2 + >>> print(value) + 1 + +Object wrappers may therefore have limited use in conjunction with literal +values. + +Type Comparison +--------------- + +The type of an instance of the object proxy will be ``ObjectProxy``, or that +of any derived class type if creating a custom object proxy. + +:: + + >>> value = 1 + >>> proxy = wrapt.ObjectProxy(value) + >>> type(proxy) + + + >>> class CustomProxy(wrapt.ObjectProxy): + ... pass + + >>> proxy = CustomProxy(1) + + >>> type(proxy) + + +Direct type comparisons in Python are generally frowned upon and allowance +for 'duck typing' preferred. Instead of direct type comparison, the +``isinstance()`` function would therefore be used. Using ``isinstance()``, +comparison of the type of the object proxy will properly evaluate against +the wrapped object. + +:: + + >>> isinstance(proxy, int) + True + +This works because the ``__class__`` attribute actually returns the class +type for the wrapped object. + +:: + + >>> proxy.__class__ + + +Note that ``isinstance()`` will still also succeed if comparing to the +``ObjectProxy`` type. It is therefore still possible to use ``isinstance()`` +to determine if an object is an object proxy. + +:: + + >>> isinstance(proxy, wrapt.ObjectProxy) + True + + >>> class CustomProxy(wrapt.ObjectProxy): + ... pass + + >>> proxy = CustomProxy(1) + + >>> isinstance(proxy, wrapt.ObjectProxy) + True + >>> isinstance(proxy, CustomProxy) + True + + +Custom Proxies +-------------- + +A custom proxy is where one creates a derived object proxy and overrides +some specific behaviour of the proxy. + +:: + + def function(): + print('executing', function.__name__) + + class CallableWrapper(wrapt.ObjectProxy): + + def __call__(self, *args, **kwargs): + print('entering', self.__wrapped__.__name__) + try: + return self.__wrapped__(*args, **kwargs) + finally: + print('exiting', self.__wrapped__.__name__) + + >>> proxy = CallableWrapper(function) + + >>> proxy() + ('entering', 'function') + ('executing', 'function') + ('exiting', 'function') + +Any method of the original wrapped object can be overridden, including +special Python methods such as ``__call__()``. If it is necessary to change +what happens when a specific attribute of the wrapped object is accessed, +then properties can be used. + +If it is necessary to access the original wrapped object from within an +overridden method or property, then ``self.__wrapped__`` is used. + +Proxy Attributes +---------------- + +When an attempt is made to access an attribute from the proxy, the same +named attribute would in normal circumstances be accessed from the wrapped +object. When updating an attributes value, or deleting the attribute, that +change will also be reflected in the wrapped object. + +:: + + >>> proxy = CallableWrapper(function) + + >>> hasattr(function, 'attribute') + False + >>> hasattr(proxy, 'attribute') + False + + >>> proxy.attribute = 1 + + >>> hasattr(function, 'attribute') + True + >>> hasattr(proxy, 'attribute') + True + + >>> function.attribute + 1 + >>> proxy.attribute + 1 + +If an attribute was updated on the wrapped object directly, that change is +still reflected in what is available via the proxy. + +:: + + >>> function.attribute = 2 + + >>> function.attribute + 2 + >>> proxy.attribute + 2 + +If creating a custom proxy and it needs to keep attributes of its own which +should not be saved through to the wrapped object, those attributes should +be prefixed with ``_self_``. + +:: + + def function(): + print('executing', function.__name__) + + class CallableWrapper(wrapt.ObjectProxy): + + def __init__(self, wrapped, wrapper): + super(CallableWrapper, self).__init__(wrapped) + self._self_wrapper = wrapper + + def __call__(self, *args, **kwargs): + return self._self_wrapper(self.__wrapped__, args, kwargs) + + def wrapper(wrapped, args, kwargs): + print('entering', wrapped.__name__) + try: + return wrapped(*args, **kwargs) + finally: + print('exiting', wrapped.__name__) + + >>> proxy = CallableWrapper(function, wrapper) + + >>> proxy._self_wrapper + + + >>> function._self_wrapper + Traceback (most recent call last): + File "", line 1, in + AttributeError: 'function' object has no attribute '_self_wrapper' + +If an attribute local to the proxy must be available under a name without +this special prefix, then a ``@property`` can be used in the class +definition. + +:: + + class CustomProxy(wrapt.ObjectProxy): + + def __init__(self, wrapped): + super(CustomProxy, self).__init__(wrapped) + self._self_attribute = 1 + + @property + def attribute(self): + return self._self_attribute + + @attribute.setter + def attribute(self, value): + self._self_attribute = value + + @attribute.deleter + def attribute(self): + del self._self_attribute + + >>> proxy = CustomProxy(1) + >>> print proxy.attribute + 1 + >>> proxy.attribute = 2 + >>> print proxy.attribute + 2 + >>> del proxy.attribute + >>> print proxy.attribute + Traceback (most recent call last): + File "", line 1, in + AttributeError: 'int' object has no attribute 'attribute' + +Alternatively, the attribute can be specified as a class attribute, with +that then being overidden if necessary, with a specific value in the +``__init__()`` method of the class. + +:: + + class CustomProxy(wrapt.ObjectProxy): + attribute = None + def __init__(self, wrapped): + super(CustomProxy, self).__init__(wrapped) + self.attribute = 1 + + >>> proxy = CustomProxy(1) + >>> print proxy.attribute + 1 + >>> proxy.attribute = 2 + >>> print proxy.attribute + 2 + >>> del proxy.attribute + >>> print proxy.attribute + None + +Just be aware that although the attribute can be deleted from the instance +of the custom proxy, lookup will then fallback to using the class attribute.