Introducing Tornado-Smack

We have been heavily using the Tornado framework for some major projects lately. Even though Tornado's class based handler approach is great for big projects, we found it cumbersome for smaller apps and prototypes.

Our solution is Tornado-Smack, which adds some syntactic sugar to Tornado. The easiest way to explain Smack is with some actual code, so let's dive in. Here is a basic Tornado implementation:

class MainHandler(tornado.web.RequestHandler):  
    def get(self, name):
        self.write("Hello, world %s " % name)

application = tornado.web.Application([  
    (r"^/foobar/(\w+)/?$", MainHandler),
])

if __name__ == "__main__":  
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

In Tornado-Smack, you can replace it with this:

from tornado_smack import App

app = App()

@app.route("/foobar/<name>")
def foobar(name):  
    return "hello world %s" % name

if __name__ == "__main__":  
    app.run(debug=True)

If you never used Tornado, this might not look like much of an improvement. The problem becomes more obvious when you have an actual, more complicated implementation in Tornado:

class MainHandler(tornado.web.RequestHandler):  
    def get(self, name):
        self.write("Hello, world %s " % name)

class SomeHandler(tornado.web.RequestHandler):  
    def get(self, name):
        self.write("Hello, world %s " % name)

    def post(self, name):
        self.write("hello you posted something")

class SomeOtherHandler(tornado.web.RequestHandler):  
    def get(self, name):
        self.write("Hello, world %s " % name)

application = tornado.web.Application([  
    (r"^/foobar/(\w+)/?$", MainHandler),
    (r"^/something/(\w+)/?$", MainHandler),
    (r"^/someotherthing/(\w+)/?$", MainHandler),
])

if __name__ == "__main__":  
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

In Smack, our implementation keeps its simplicity:

from tornado_smack import App

app = App()

@app.route("/foobar/<name>")
def foobar(name):  
    return "hello world %s" % name

@app.route("/something/<name>", methods=['GET', 'POST'])
def something(name):  
    return "hello world %s" % name

@app.route("/somethingelse/<name>")
def somethingelse(name):  
    return "hello world %s" % name

if __name__ == "__main__":  
    app.run(debug=True)

I think this is much easier and more fun to write than the alternative.

Now, I can hear you say: "Why don't you just use flask or something?" My answer is: Tornado's async implementation is amazing. You can also use gevent-flask but I am crazy about patching my sockets. It's really not so easy to have a proper async implementation in any other framework than Tornado, since alternatives don't really expose the event loop.

Here is a super simple example with Tornado-Smack:

@app.route("/job/<name>")
@coroutine
def wait_for_job(self, img_id, cmd_md5, name):  
    data = redis.get(name)
    # wait until you have data, but don't block other requests
    while not data:
        yield gen.Task(IOLoop.instance().add_timeout, time.time() + 0.200)
        data = redis.get(name)
    data = json.loads(data)
    self.write(data)
    self.finish()

Tornado is great for async jobs, but every time I find myself copying regular expressions, changing names of handler classes. So I thought with a bit of meta programming, I could easily change this approach without touching the internals of Tornado.

The first working implementation for Smack came in a couple of hours, in less than 50 lines. Then I added a bunch of other stuff, like Werkzeug's debugger, some proxy methods for accessing current RequestHandler etc. After some tests were added, it was ready for prime time!

Some might prefer using Tornado's style but especially for small projects, trying, prototyping things, Smack became really handy. I thought if we are using this at Hipo, someone might find it useful too, so we are releasing this as open source.

Here is the documentation: http://tornado-smack.readthedocs.org/en/latest/

Here is the source: https://github.com/Hipo/tornado-smack

Stay tuned on Twitter or subscribe to the Hipo newsletter to keep up with our updates.