Source code for django_auxilium.utils.functools.decorators

"""
Collection of simple utilities which modify some behaviour of functions
"""

from __future__ import print_function, unicode_literals
import inspect
import logging
from functools import partial, wraps


log = logging.getLogger(__name__)


[docs]class Decorator(object): """ Base class for various decorators. Its aim is to do the same for decorators as Django class-based-views did to function-based-views. The most common pattern for creating decorators in Python can be summarized in the following snippet:: from functools import wraps def decorator(f): @wraps(f) def wrapper(*args, **args): # some logic here return f(*args, **kwargs) return wrapper The problem with the above approach is when decorators need to employ more difficult logic. That sometimes can cause decorator functions to become big hence making it difficult to maintain and test. This class is meant to simplify this task. It provides a way to make decorators by using classes. Its aim is to do the same for decorators as Django class-based-views did to function-based-views. This means that decorator's various functionality can be split among class methods which can make the decorator's logic more transparent and more test-friendly. It also automatically wraps the decorated object using ``functools.wraps`` saving some coding lines. Finally, this class allows to create decorators where initializing the decorator is optional. This perhaps can better be illustrated in a snippet:: @decorator # not initialized def happy(): pass @decorator(foo='bar') # initialized def bunnies(): pass Using this class is pretty simple. The most useful method you should know is :py:meth:`.get_wrapped_object`. Inside the method you can refer to :py:attr:`.to_wrap` which is the object the method should wrap. The method itself should return the wrapped version of the :py:attr:`.to_wrap` object. If your decorator needs to accept additional parameters, you can overwrite decorators ``__init__``. Finally, to use the decorator, don't forget to use the :py:meth:`as_decorator` class method. You can refer to **Examples** to see a more full example of how to use this class. Examples -------- :: >>> class D(Decorator): ... def __init__(self, happy=False): ... self.happy = happy ... def get_wrapped_object(self): ... def wrapped(*args, **kwargs): ... print('called wrapped with', args, kwargs) ... if self.happy: ... print('Bunnies are very happy today!') ... return self.to_wrap(*args, **kwargs) ... return wrapped ... >>> decorator = D.as_decorator() >>> # not initialized (default values will be used if defined) >>> @decorator ... def sum(a, b): ... return a + b >>> sum(5, 6) called wrapped with (5, 6) {} 11 >>> # initialized with no parameters (default values will be used if defined) >>> @decorator() ... def sum(a, b): ... return a + b >>> sum(7, 8) called wrapped with (7, 8) {} 15 >>> # initialized with keyword parameters >>> @decorator(happy=True) ... def sum(a, b): ... "Sum function" ... return a + b >>> sum(9, 10) called wrapped with (9, 10) {} Bunnies are very happy today! 19 >>> sum.__doc__ == 'Sum function' True Attributes ---------- to_wrap : object The object to be decorated/wrapped. This attributes becomes available when the decorator is called on the object. """ def __call__(self, f): self.to_wrap = f self.pre_wrap() if inspect.isclass(f): return self.get_wrapped_object() else: return wraps(f)(self.get_wrapped_object())
[docs] def pre_wrap(self): """ Hook for executing things before wrapping objects """
[docs] def get_wrapped_object(self): """ Returns the wrapped version of the object to be decorated/wrapped. This is the meat of class because this method is the one which creates the decorator. Inside the method, you can refer to the object to be decorated via ``self.to_wrap``. .. note:: This class automatically uses the ``functools.wraps`` to preserve the ``to_wrap``'s object useful attributes such as ``__doc__`` hence there is no need to do that manually. You can just return a wrapped object and the class will take care of the rest. Returns ------- f : object Decorated/wrapped function """ return self.to_wrap
[docs] @classmethod def as_decorator(cls, *a, **kw): """ Return the actual decorator. This method is necessary because the decorator is a class. Consider the following:: >>> class ClassDecorator(object): ... def __init__(self, obj): ... self.to_wrap = obj >>> @ClassDecorator ... def foo(): pass >>> isinstance(foo, ClassDecorator) True In the above, since ``foo`` will be passed to the decorator's class ``__init__``, the returned object will be an instance of the decorator's class instead of a wrapper function. To avoid that, this method constructs a wrapper function which guarantees that the output of the decorator will be the wrapped object:: >>> class D(Decorator): ... pass >>> d = D.as_decorator() >>> @d ... def foo(): pass >>> isinstance(foo, D) False """ klass = partial(cls, *a, **kw) @wraps(cls) def wrapper(*args, **kwargs): if args and (callable(args[0]) or inspect.isclass(args[0])): return klass()(args[0]) else: return klass(*args, **kwargs) return wrapper
[docs]class HybridDecorator(Decorator): """ Class for implementing decorators which can decorate both regular functions as well as class methods. This decorator automatically determines if the object to be decorated is a stand-alone function or a class method by adding an :py:attr:`in_class` attribute. Parameters ---------- is_method : bool, optional Explicitly specify whether the decorator is being used on class method or a standalone function. When not specified, :py:meth:`.pre_wrap` is used to automatically determine that. Please look at its documentation for the explanation of its limitations. Attributes ---------- in_class : bool ``True`` if the object to be decorated is a class method, or ``False`` if it is a standalone function """ def __init__(self, is_method=None): self.is_method = None
[docs] def pre_wrap(self): """ Method which determines whether the ``to_wrap`` is a class method or a standalone function .. warning:: This method uses pretty primitive technique to determine whether the wrapped callable is a class method or a function and so it might not work in all cases. It checks the first parameter name of the callable and if it is either ``'self'`` or ``'cls'`` it is most likely a method. If you need more precise behavior you are encouraged to use ``is_method`` decorator parameter. """ if self.is_method is None: try: arg = inspect.getargspec(self.to_wrap).args[0] except IndexError: self.in_class = False else: self.in_class = arg in ['self', 'cls'] else: self.in_class = self.is_method