WSGI decorators considered inconvenient
In the grand tradition of "considered harmful" essays, here's one on why it's not a great idea to use Python function decorators to wrap WSGI applications (e.g. for authentication/authorization). But unlike using goto to implement control structures, the mixture of decorators and WSGI is just inconvenient. In this essay, I'll explore the problem, show how it applies more generally to function decorators that work with arguments, and suggest a hackish solution if you don't want to avoid WSGI decorators altogether.
WSGI applications are functions (well, callables) that expect two arguments (environ, start_response): a dictionary of CGI-style and extension variables and a callback function. Pop quiz: in what position in a function's argument list is the environ dict?
The correct answer is "maybe 0 and maybe 1," and this is the start of the problems with WSGI decorators. Specifically, when you're calling a WSGI function from normal code, environ is in position 0; and when you're implementing a WSGI callable as an ordinary function, it's in position 0; but when you're implementing a WSGI callable as a method of a class, it's in position 1 because there's a self argument before it.
When you wrap a bound method in another function, as is commonly done with WSGI middleware, there's no problem. Bound methods carry their self arguments with them, in their im_self attribute. The argument signature looks the same from the outside.
In a decorator, though, you do have to worry about the self argument. When you apply a decorator to a function inside a class, the decorator receives a reference to a function that has not become a method yet. In fact, only the final return value of the last decorator on the function will be "blessed" into an unbound method of the class, after the class block closes. From the decorator you can't tell whether the function is destined to be a method or not.
What this means is that, if your decorator expects a particular argument signature such as (environ, start_response), whenever the decorator is applied to a method, the arguments will be shifted over one place. And you don't know that this has happened unless you (1) check the length of the argument list, assuming there are no optional arguments, or (2) check the types of the arguments.
Our method-safe WSGI decorator might look like this:
import functools
def wsgi_decorator(function):
@functools.wraps(function)
def wrapper(*args):
if len(args) == 3:
environ, start_response = args[1:]
else:
environ, start_response = args
environ['mykey'] = "my value" # Do stuff
return function(*args)
return wrapper
By checking the length of *args, we sidestep the problem of the potential self argument. You can see that the above decorator works through the following example:
import webtest
import webob
@wsgi_decorator
def app(environ, start_response):
return webob.Response(environ['mykey'])(environ, start_response)
class C(object):
@wsgi_decorator
def app(self, environ, start_response):
return webob.Response(environ['mykey'])(environ, start_response)
print webtest.TestApp(app).get('/').body
print webtest.TestApp(C().app).get('/').body
This approach can be simplified with a simple utility function:
def wsgi_args(args):
if len(args) == 3:
return args[1:3]
else:
return args[0:2]
The original function, however, must still be called with *args, not environ, start_response, because it will expect a self argument if it's a method.
If you use decorators that ship with third-party WSGI modules, they might work with only functions, only modules, or both. Sometimes the only way to be sure is to check the source code.
And unfortunately, other cases are not as simple as this. The example above only works because WSGI applications are expected to have exactly two regular arguments. If the expected argument signature involves optional arguments, it gets trickier. It may be a better idea to avoid such decorators.
Tweet it!
Post Comment
All comments are personally reviewed and must be: