From d829f6f6503ee875b37d25dff83f17a514fa27f0 Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Mon, 7 Oct 2013 17:02:56 +1100 Subject: [PATCH] Document FunctionWrapper. --- docs/wrappers.rst | 152 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/docs/wrappers.rst b/docs/wrappers.rst index 3772f65..dad798e 100644 --- a/docs/wrappers.rst +++ b/docs/wrappers.rst @@ -318,3 +318,155 @@ that then being overidden if necessary, with a specific value in the 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. + +Function Wrappers +----------------- + +Although an ``ObjectProxy`` can be used to wrap a function, it doesn't do +anything special in respect of bound methods. If attempting to use a custom +object proxy to wrap instance methods, class methods or static methods, it +would be necessary to override the appropriate descriptor protocol methods +in order to be able to intercept and modify in any way the execution of the +wrapped function. + +:: + + class BoundCallableWrapper(ObjectProxy): + + def __init__(self, wrapped, wrapper): + super(BoundCallableWrapper, self).__init__(wrapped) + self._self_wrapper = wrapper + + def __get__(self, instance, owner): + return self + + def __call__(self, *args, **kwargs): + return self._self_wrapper(self.__wrapped__, args, kwargs) + + class CallableWrapper(ObjectProxy): + + def __init__(self, wrapped, wrapper): + super(CallableWrapper, self).__init__(wrapped) + self._self_wrapper = wrapper + + def __get__(self, instance, owner): + function = self.__wrapped__.__get__(instance, owner) + return BoundCallableWrapper(function, self._self_wrapper) + + def __call__(self, *args, **kwargs): + return self._self_wrapper(self.__wrapped__, args, kwargs) + +The ``CallableWrapper.__call__()`` method would therefore be invoked when +``CallableWrapper`` is used around a regular function. The +``BoundCallableWrapper.__call__()`` would instead be what is invoked for a +bound method, the instance of ``BoundCallableWrapper`` having being created +when the original wrapped method was bound to the class instance. + +This specific pattern is actually the basis of what is required to +implement a robust function wrapper for use in implementing a decorator. +Because it is a fundamental pattern, a predefined version is available as +``wrapt.FunctionWrapper``. + +As with the illustrative example above, ``FunctionWrapper`` class accepts +two key arguments: + +* ``wrapped`` - The function being wrapped. +* ``wrapper`` - A wrapper function to be called when the wrapped function is invoked. + +Although in prior examples the wrapper function was shown as accepting three +positional arguments of the wrapped function and the args and kwargs for when +the wrapped function was called, when using ``FunctionWrapper``, it is +expected that the wrapper function accepts four arguments. These are: + +* ``wrapped`` - The wrapped function which in turns needs to be called by your wrapper function. +* ``instance`` - The object to which the wrapped function was bound when it was called. +* ``args`` - The list of positional arguments supplied when the decorated function was called. +* ``kwargs`` - The dictionary of keyword arguments supplied when the decorated function was called. + +When ``FunctionWrapper`` is applied to a normal function or static method, +the wrapper function when called will be passed ``None`` as the +``instance`` argument. + +When applied to an instance method, the wrapper function when called will +be passed the instance of the class the method is being called on as the +``instance`` argument. This will be the case even when the instance method +was called explicitly via the class and the instance passed as the first +argument. That is, the instance will never be passed as part of ``args``. + +When applied to a class method, the wrapper function when called will be +passed the class type as the ``instance`` argument. + +When applied to a class, the wrapper function when called will be passed +``None`` as the ``instance`` argument. The ``wrapped`` argument in this +case will be the class. + +The above rules can be summarised with the following example. + +:: + + import inspect + + def wrapper(wrapped, instance, args, kwargs): + if instance is None: + if inspect.isclass(wrapped): + # Decorator was applied to a class. + return wrapped(*args, **kwargs) + else: + # Decorator was applied to a function or staticmethod. + return wrapped(*args, **kwargs) + else: + if inspect.isclass(instance): + # Decorator was applied to a classmethod. + return wrapped(*args, **kwargs) + else: + # Decorator was applied to an instancemethod. + return wrapped(*args, **kwargs) + +Using these checks it is therefore possible to create a universal function +wrapper that can be applied in all situations. It is no longer necessary to +create different variants of function wrappers for normal functions and +instance methods. + +In all cases, the wrapped function passed to the wrapper function is called +in the same way, with ``args`` and ``kwargs`` being passed. The +``instance`` argument doesn't need to be used in calling the wrapped +function. + +A simple decorator factory implementation which makes use of +``FunctionWrapper`` to delegate execution of the wrapped function to +the wrapper function would be: + +:: + + def function_wrapper(wrapper): + @functools.wraps(wrapper) + def _wrapper(wrapped): + return FunctionWrapper(wrapped, wrapper) + return _wrapper + +It would be used like: + +:: + + @function_wrapper + def wrapper(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @wrapper + def function(): + pass + +This example of a simplified decorator factory is made available as +``wrapt.function_wrapper``. Although it is usuable in its own right, it is +preferable that ``wrapt.decorator`` be used to create decorators as it +provides additional features. The ``@function_wrapper`` decorator would +generally be used more when performing monkey patching and needing to +dynamically create function wrappers. + +:: + + @function_wrapper + def wrapper(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + callback = wrapper(fetch_callback())