diff --git a/docs/decorators.rst b/docs/decorators.rst new file mode 100644 index 0000000..1c677c0 --- /dev/null +++ b/docs/decorators.rst @@ -0,0 +1,473 @@ +Decorators +========== + +The **wrapt** module provides various components, but the main reason that +it likely is to be used is for creating decorators. This document covers the +creation of decorators and all the information needed to cover what you can +do within the wrapper function linked to your decorator. + +Creating Decorators +------------------- + +To implement your decorator you need to first define a wrapper function. +This will be called each time a decorated function is called. The wrapper +function needs to take four positional arguments: + +* ``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. + +The wrapper function would do whatever it needs to, but would usually in +turn call the wrapped function that is passed in via the ``wrapped`` +argument. + +The decorator ``@wrapt.decorator`` then needs to be applied to the wrapper +function to convert it into a decorator which can in turn be applied to +other functions. + +:: + + import wrapt + + @wrapt.decorator + def pass_through(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @pass_through + def function(): + pass + +Decorators With Arguments +------------------------- + +If you wish to implement a decorator which accepts arguments, then wrap the +definition of the decorator in a function closure. Any arguments supplied +to the outer function when the decorator is applied, will be available to +the inner wrapper when the wrapped function is called. + +:: + + import wrapt + + def with_arguments(myarg1, myarg2): + @wrapt.decorator + def wrapper(wrapped, instance, args, kwargs): + return wrapper(*args, **kwargs) + return wrapper + + @with_arguments(1, 2) + def function(): + pass + +If using Python 3, you can use the keyword arguments only syntax to force +use of keyword arguments when the decorator is used. + +:: + + import wrapt + + def with_keyword_only_arguments(*, myarg1, myarg2): + @wrapt.decorator + def wrapper(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + return wrapper + + @with_keyword_only_arguments(myarg1=1, myarg2=2) + def function(): + pass + +Processing Function Arguments +----------------------------- + +The original set of positionl arguments and keyword arguments supplied when +the decorated function is called will be passed in the ``args`` and +``kwargs`` arguments. + +Note that these are always passed as their own unique arguments and are not +broken out and bound in any way to the decorator wrapper arguments. In +other words, the decorater wrapper function signature must always be:: + + @wrapt.decorator + def my_decorator(wrapped, instance, args, kwargs): # CORRECT + return wrapped(*args, **kwargs) + +You cannot use:: + + @wrapt.decorator + def my_decorator(wrapped, instance, *args, **kwargs): # WRONG + return wrapped(*args, **kwargs) + +nor can you specify actual named arguments to which ``args`` and ``kwargs`` +would be bound. + +:: + + @wrapt.decorator + def my_decorator(wrapped, instance, arg1, arg2): # WRONG + return wrapped(arg1, arg2) + +Separate arguments are used and no binding performed to avoid the +possibility of name collisions between the arguments passed to a decorated +function when called, and the names used for the ``wrapped`` and +``instance`` arguments. This can happen for example were ``wrapped`` and +``instance`` also used as keyword arguments by the wrapped function. + +If needing to modify certain arguments being supplied to the decorated +function when called, you will thus need to trigger binding of the +arguments yourself. This can be done using a nested function which in turn +then calls the wrapped function:: + + @wrapt.decorator + def my_decorator(wrapped, instance, args, kwargs): + def _execute(arg1, arg2, *_args, **_kwargs): + + # Do something with arg1 and arg2 and then pass the + # modified values to the wrapped function. Use 'args' + # and 'kwargs' on the nested function to mop up any + # unexpected or non required arguments so they can + # still be passed through to the wrapped function. + + return wrapped(arg1, arg2, *_args, **_kwargs) + + return _execute(*args, **kwargs) + +If you do not need to modify the arguments being passed through to the +wrapped function, but still need to extract them so as to log them or +otherwise use them as input into some process you could instead use. + +:: + + @wrapt.decorator + def my_decorator(wrapped, instance, args, kwargs): + def _arguments(arg1, arg2, *args, **kwargs): + return (arg1, arg2) + + arg1, arg2 = _arguments(*args, **kwargs) + + # Do something with arg1 and arg2 but still pass through + # the original arguments to the wrapped function. + + return wrapped(*args, **kwargs) + +You should not simply attempt to extract positional arguments from ``args`` +directly because this will fail if those positional arguments were actually +passed as keyword arguments, and so were passed in ``kwargs`` with ``args`` +being an empty tuple. + +Function Argument Specifications +-------------------------------- + +To obtain the argument specification of a decorated function the standard +``getargspec()`` function from the ``inspect`` module can be used. + +:: + + @wrapt.decorator + def my_decorator(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @my_decorator + def function(arg1, arg2): + pass + + >>> print(inspect.getargspec(function)) + ArgSpec(args=['arg1', 'arg2'], varargs=None, keywords=None, defaults=None) + +If using Python 3, the ``getfullargspec()`` or ``signature()`` functions +from the ``inspect`` module can also be used. + +In other words, applying a decorator created using ``@wrapt.decorator`` to +a function is signature preserving and does not result in the loss of the +original argument specification as would occur when more simplistic +decorator patterns are used. + +Wrapped Function Documentation +------------------------------ + +To obtain documentation for a decorated function which may be specified in +a documentation string of the original wrapped function, the standard +Python help system can be used. + +:: + + @wrapt.decorator + def my_decorator(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @my_decorator + def function(arg1, arg2): + """Function documentation.""" + pass + + >>> help(function) + Help on function function in module __main__: + + function(arg1, arg2) + Function documentation. + +Just the documentation string itself can still be obtained by accessing the +``__doc__`` attribute of the decorated function. + +:: + + >>> print(function.__doc__) + Function documentation. + +Wrapped Function Source Code +---------------------------- + +To obtain the argument specification of a decorated function the standard +``getsource()`` function from the ``inspect`` module can be used. + +:: + + @wrapt.decorator + def my_decorator(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @my_decorator + def function(arg1, arg2): + pass + + >>> print(inspect.getsource(function)) + @my_decorator + def function(arg1, arg2): + pass + +As with signatures, the use of the decorator does not prevent access to the +original source code for the wrapped function. + +Signature Changing Decorators +----------------------------- + +When using ``inspect.getargspec()`` the argument specification for the +original wrapped function is returned. If however the decorator is a +signature changing decorator, this is not going to be what is desired. + +In this circumstance it is necessary to pass a dummy function to the +decorator via the optional ``adapter`` argument. When this is done, the +argument specification will be sourced from the prototype for this dummy +function. + +:: + + def _my_adpater_prototype(arg1, arg2): pass + + @wrapt.decorator(adapter=_my_adpater_prototype) + def my_adapter(wrapped, instance, args, kwargs): + """Adapter documentation.""" + + def _execute(arg1, arg2, *_args, **_kwargs): + + # We actually multiply the first two arguments together + # and pass that in as a single argument. The prototype + # exposed by the decorator is thus different to that of + # the wrapped function. + + return wrapped(arg1*arg2, *_args, **_kwargs) + + return _execute(*args, **kwargs) + + @my_adapter + def function(arg): + """Function documentation.""" + + pass + + >>> help(function) + Help on function function in module __main__: + + function(arg1, arg2) + Function documentation. + +As it would not be accidental that you applied such a signature changing +decorator to a function, it would normally be the case that such usage +would be explained within the documentation for the wrapped function. As +such, the documentation for the wrapped function is still what is used for +the ``__doc__`` string and what would appear when using the Python help +system. In the latter, the arguments required of the adapter would though +instead appear. + +Decorating Functions +-------------------- + +When applying a decorator to a normal function, the ``instance`` argument +would always be ``None``. + +:: + + @wrapt.decorator + def pass_through(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @pass_through + def function(arg1, arg2): + pass + + function(1, 2) + +Decorating Instance Methods +--------------------------- + +When applying a decorator to an instance method, the ``instance`` argument +will be the instance of the class on which the instance method is called. +That is, it would be the same as ``self`` passed as the first argument to +the actual instance method. + +:: + + @wrapt.decorator + def pass_through(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + class Class(object): + + @pass_through + def function_im(self, arg1, arg2): + pass + + c = Class() + + c.function_im(1, 2) + + Class.function_im(c, 1, 2) + +Note that the ``self`` argument is only passed via ``instance``, it is not +passed as part of ``args``. Only the arguments following on from the ``self`` +argument will be a part of args. + +When calling the wrapped function in the decorator wrapper function, the +``instance`` should never be passed explicitly though. This is because the +instance is already bound to ``wrapped`` and will be passed automatically +as the first argument to the original wrapped function. + +This is even the situation where the instance method was called via the +class type and the ``self`` pointer passed explicitly. This is the case +as the decorator identifies this specific case and adjusts ``instance`` +and ``args`` so that the decorator wrapper function does not see it as +being any different to where it was called directly on the instance. + +Decorating Class Methods +------------------------ + +When applying a decorator to a class method, the ``instance`` argument will +be the class type on which the class method is called. That is, it would be +the same as ``cls`` passed as the first argument to the actual class +method. + +:: + + @wrapt.decorator + def pass_through(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + class Class(object): + + @pass_through + @classmethod + def function_cm(cls, arg1, arg2): + pass + + Class.function_cm(1, 2) + +Note that the ``cls`` argument is only passed via ``instance``, it is not +passed as part of ``args``. Only the arguments following on from the ``cls`` +argument will be a part of args. + +When calling the wrapped function in the decorator wrapper function, the +``instance`` should never be passed explicitly though. This is because the +instance is already bound to ``wrapped`` and will be passed automatically +as the first argument to the original wrapped function. + +Note that due to a bug in Python ``classmethod.__get__()``, whereby it does +not apply the descriptor protocol to the function wrapped by ``classmethod``, +the above only applies where the decorator wraps the ``@classmethod`` +decorator. If the decorator is placed inside of the ``@classmethod`` +decorator, then ``instance`` will be ``None`` and the decorator wrapper +function will see the call as being the same as a normal function. As a +result, always place any decorator outside of the ``@classmethod`` +decorator. + +Decorating Static Methods +------------------------- + +When applying a decorator to a static method, the ``instance`` argument +will be ``None``. In other words, the decorator wrapper function will not +be able to distinguish a call to a static method from a normal function. + +:: + + @wrapt.decorator + def pass_through(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + class Class(object): + + @pass_through + @staticmethod + def function_sm(arg1, arg2): + pass + + Class.function_sm(1, 2) + +Decorating Classes +------------------ + +When applying a decorator to a class, the ``instance`` argument will be +``None``. In order to distinguish this case from a normal function call, +``inspect.isclass()`` should be used on ``wrapped`` to determine if it +is a class type. + +:: + + @wrapt.decorator + def pass_through(wrapped, instance, args, kwargs): + return wrapped(*args, **kwargs) + + @pass_through + class Class(object): + pass + + c = Class() + +Universal Decorators +-------------------- + +A universal decorator is one that can be applied to different types of +functions and can adjust automatically based on what is being decorated. + +For exapmple, the decorator may be able to be used on both a normal +function and an instance method, thereby avoiding the need to create two +separate decorators to be used in each case. + +A universal decorator can be created by observing what has been stated +above in relation to the expected values/types for ``wrapped`` and +``instance`` passed to the decorator wrapper function. + +These rules can be summarised by the following. + +:: + + import inspect + + @wrapt.decorator + def universal(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) + +To be truly robust, a universal decorator should raise a runtime exception +at the point it is subsequently called, when it was applied as a decorator +in a scenario it does not support. diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..136e5af --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,2 @@ +Code Examples +============= diff --git a/docs/index.rst b/docs/index.rst index cc10d9f..54c78cc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,6 +29,11 @@ Contents :maxdepth: 1 quick-start + decorators benchmarks known-issues unit-testing + +.. proxies +.. wrappers +.. examples diff --git a/docs/proxies.rst b/docs/proxies.rst new file mode 100644 index 0000000..e11229c --- /dev/null +++ b/docs/proxies.rst @@ -0,0 +1,2 @@ +Object Proxies +============== diff --git a/docs/wrappers.rst b/docs/wrappers.rst new file mode 100644 index 0000000..661671c --- /dev/null +++ b/docs/wrappers.rst @@ -0,0 +1,2 @@ +Function Wrappers +=================