Create a custom Tornado decorator with functools
Decorators are neat. They let you do things in much the same way javascript’s anonymous functions work.
So why use a decorator? Why not create an override (or overload, depending on your vernacular) to accomplish the same sort of goal?
I had a problem today where I needed to ensure a specific variable was coming over in a query string argument in the wonderful tornado framework. I didn’t want to have to reinvent the wheel, or have to overwrite the concept of simplicity in the Web class in the framework. Programmers in general are lazy people, and like doing a lot with a little, so I decided to see how tornado handled it’s decorators.
Before I dig into the code, a few notes:
A decorator is the name used for a software design pattern. Decorators dynamically alter the functionality of a function, method, or class without having to directly use subclasses or change the source code of the function being decorated.
Make sense? It wasn’t really clear to me either. But I’ve used anonymous functions in javascript to alter a function without interfering with the global object, so why can’t we do something similar in Python? Well- It turns out; We can.
Here’s what tornado does to handle their @authenticated decorator. (for authenticating a user before executing the route)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | def authenticated(method): """Decorate methods with this to require that the user be logged in.""" @functools.wraps(method) def wrapper(self, *args, **kwargs): if not self.current_user: if self.request.method in ("GET", "HEAD"): url = self.get_login_url() if "?" not in url: if urlparse.urlsplit(url).scheme: # if login url is absolute, make next absolute too next_url = self.request.full_url() else: next_url = self.request.uri url += "?" + urllib.urlencode(dict(next=next_url)) self.redirect(url) return raise HTTPError(403) return method(self, *args, **kwargs) return wrapper |
Whoa. tons of stuff going on here. Lets break it down.
First, it’s important to note that this lives OUTSIDE of any particular class.
1 2 3 4 5 6 7 | def authenticated(method): """Decorate methods with this to require that the user be logged in.""" @functools.wraps(method) def wrapper(self, *args, **kwargs): implementation_details() return method(self, *args, **kwargs) return wrapper |
Wait, what? @functools?
The functools module is for higher-order functions: functions that act on or return other functions. In general, any callable object can be treated as a function for the purposes of this module.
Get that? for any javascript developers, this may seem familar. This is somewhat similar to setting up an anonymous function, right?
The most important thing to remember here is that .wraps() is being fed another method and spitting out that method if conditions or whatever is met during the wrappers execution. You don’t necessarily have to return the function (as explained below) but you probably should.
In the implementation I built for my app, I needed an easy way to throw an error when a condition wasn’t met during the request’s execution. Since tornado kind of already does this through it’s @authenticated decorator, it makes sense to reuse that bit of magic for my own purposes.
Lets go over some boilerplate to defining our own methods for use in tornado.
For this example, I’ve built a simple request blocker that returns some json if a user isn’t allowed to hit an API call.
1 2 3 4 5 6 7 8 9 10 | def block_jerks(method): """The black knight always triumphs!""" @functools.wraps(method) def wrapper(self, *args, **kwargs): should_i_block_this_request = self.check_if_i_should() if should_i_block_this_request: return self.you_have_been_blocked() else: return method(self, *args, **kwargs) return wrapper |
Then later on, when i want to actually use this decorator, I do the following:
1 2 3 4 | class SomeHandler(WebRequest): @block_jerks def get(self, param): self.write('tis but a scratch') |
Of course, this assumes that you have a global subclass of tornado.web.RequestHandler (WebRequest above) somewhere in your project. You should probably do that to avoid violating DRY. When you have something of that sort, just import the decorator like you would a class in your file and you’re good to go.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import functools import tornado.web def block_jerks(method): """The black knight always triumphs!""" @functools.wraps(method) def wrapper(self, *args, **kwargs): should_i_block_this_request = self.check_if_i_should() if should_i_block_this_request: return self.you_have_been_blocked() else: return method(self, *args, **kwargs) return wrapper class WebRequest(tornado.web.RequestHandler): pass class SomeHandler(WebRequest): @block_jerks def get(self, param): self.write('tis but a scratch') |
That’s really it. We now have a decorator in place that we can use anywhere in our routes. (because we’re using a project-wide subclass of RequestHandler, right?)


Recent Comments