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