Hitting a new level of meta, I just wrote a decorator decorator for Django views.
The context here is that I needed to write some custom view decorators that gave me access to the
request object, since the normal built-in decorators like
user_passes_test only offer access to the
request.user object. While doing that, I wanted them to be able to apply to both function- and class-based views without having to use something like the
method_decorator decorator, but still having it know to wrap
dispatch on a class-based view unless otherwise specified.
Enter the decorator decorator:
from functools import wraps from types import FunctionType def class_or_function_view_decorator(class_method_to_wrap='dispatch'): ''' A decorator that enables other decorators to decorate both function- and class-based views. Can also be used on class-based views' methods. Usage: @class_or_function_view_decorator() def sayhello(f): """ A decorator that says hello before the function is called. When used on a class, it will wrap the class's `dispatch` method. Usage: @sayhello def saybye(): print 'bye!' @sayhello def MyClass(object): def dispatch(self): print 'bye!' """ @functools.wraps(f) def wrapped(*args, **kwargs): print 'hello!' return f(*args, **kwargs) return wrapped @class_or_function_view_decorator('__init__') def saybye(f): """ When this decorator is used on a class, it will wrap the class's `__init__` method, unlike the previous example which wrapped `dispatch`. """ @functools.wraps(f) def wrapped(*args, **kwargs): x = f(*args, **kwargs) print 'bye!' return x return wrapped This decorator is used to decorate a view decorator to enable it to be used on function-based views or on class-based views. If the resulting decorator is used on class-based views, you can choose which method of the class it will decorate, defaulting to 'dispatch' if not provided. If used on function-based views, the resulting decorator works as normal. ''' def outside_wrapper(f): @wraps(f) def wrapped(to_be_wrapped): if type(to_be_wrapped) is FunctionType: return f(to_be_wrapped) else: setattr(to_be_wrapped, class_method_to_wrap, f(getattr(to_be_wrapped, class_method_to_wrap))) return to_be_wrapped return wrapped return outside_wrapper
In short, this allows me to create a single decorator that can be used anywhere, like so:
@class_or_function_view_decorator() def request_passes_test(test_func, login_url=None): """ Decorator for views that checks that the session passes the given test, redirecting to the log-in page if necessary. The test should be a callable that takes the request object and returns True if the test passes. """ def decorator(view_func): @wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view(request, *args, **kwargs): if test_func(request): return view_func(request, *args, **kwargs) else: if login_url: return redirect(login_url) else: raise PermissionDenied() return _wrapped_view return decorator