Facebook Tornado's Excellent Python Templates

20 Feb 2010

The python Torando webserver and framework from the friendly folks at Facebook/FriendFeed comes with a templating system. You don't need to use the server in order to use the templates.

The template module were designed to be used by developers that allow the full power of python. These are not Django-style templates, which could be handed off to interactive designer to hack around on.

For other python-based templates, I've used Mako before since it seemed the simplest of the bunch. But this is a dozen files, crazy parsers and abstract syntax tree representation. Cheetah templates has 30 files, and so on.

Tornado templates? It's only two files. template.py which is the core. And escape.py provides common functions you can use in your template (e.g. url_escape). That's it.

The Templates

Here's a sample:

from tornado.template import Template

ts1 = """
<html><head>
<title> {{ title }} </title>
</head><body>

{% if True %}
<h1>Hello World</h1>
{% else %}
<h1>Goodbye Cruel World</h1>
{% end %}

</body></html>
"""

t1 = Template(ts1, name="ts1", compress_whitespace=True)

print(t1.code)

def _execute():
    _buffer = []
    _buffer.append('\n<html><head>\n<title> ')
    _tmp = title
    if isinstance(_tmp, str): _buffer.append(_tmp)
    elif isinstance(_tmp, unicode): _buffer.append(_tmp.encode('utf-8'))
    else: _buffer.append(str(_tmp))
    _buffer.append(' </title>\n</head><body>\n')
    if True:
        _buffer.append('\n<h1>Hello World</h1>\n')
    else:
        _buffer.append('\n<h1>Goodbye Cruel World</h1>\n')
    _buffer.append('\n</body></html>\n')
    return ''.join(_buffer)

That python code is generated once, then compiled for subsequent rendering, so it's as fast as python can be. Oh yeah, rendering. Use t1.generate(title="foo" , ...other named args...). You can use all sorts of other control constructs in the template: try, for, import, etc.

But "template inheritance" is where it gets interesting. Wrap parts of the template in {% block name %} and another {% end %}. Now you can over-ride these blocks in another template.

ts1 = """
<html><head>
<title> {{ title }} </title>
</head><body>
{% block body %}
{% if True %}
<h1>Hello World</h1>
{% else %}
<h1>Goodbye Cruel World</h1>
{% end %}
{% end %}
</body></html>
"""

(oh with the added block the generated code doesn't actually change)

Now lets make another template. We'll need a Loader so we can reference other templates by name. The Tornado webserver does all this with loader that uses files. For the example, ours is, uhhh, more simple:

ts2 = """                                                                                                                    
{% extends ts1 %}                                                                                                            
{% block body %}                                                                                                             
<h1>Hello World</h1>                                                                                                      
{% end %}                                                                                                                    
"""

class Loader(object):
    def load(self, name, parent_path=None):
        return t1

t2 = Template(ts2, name="ts2", loader=Loader(), compress_whitespace=True)

print(t2.code)

Results in the following code:

def _execute():
    _buffer = []
    _buffer.append('\n<html><head>\n<title> ')
    _tmp = title
    if isinstance(_tmp, str): _buffer.append(_tmp)
    elif isinstance(_tmp, unicode): _buffer.append(_tmp.encode('utf-8'))
    else: _buffer.append(str(_tmp))
    _buffer.append(' </title>\n</head><body>\n')
    _buffer.append('\n<h1>Hello World</h1>\n')
    _buffer.append('\n</body></html>\n')
    return ''.join(_buffer)

Ta-Da!

Conclusion

Oh yeah! True, error handling isn't great, but really it's not that complicated. It took me a few hours to swap out from Mako.

Use the source or pydoc tornado.tempalte for more details