Simple Django-like Templating
October 2006 | Fredrik Lundh
I’m currently working on a new version of the ludicrously simple content management system I’m using for zone.effbot.org, and wanted a template system that was a bit more flexible than the old replace-based hack I’d been using.
I ended up implementing a small subset of the Django template syntax (dead link), partially because the syntax is pretty okay, partially because the Django’s template rendering model provides a separation between templates and code that has worked very well in earlier projects. (But mostly because it’s fun to play with minimalistic reimplementations of larger designs. Do you really need all that code?)
Here’s a small Django template (a simplified version of an example from the Django documentation):
<h1>{{ section.title }}</h1> <h2> <a href="{{ story.get_absolute_url }}"> {{ story.headline|upper }} </a> </h2> <p>{{ story.tease|truncatewords:"100" }}</p>
The {{ … }} sections are Django variable markers, which are replaced with the specified content when the template is rendered.
The variables are looked up in a given rendering context, and the dot notation is used as a general access mechanism; what looks like an attribute can be either a key in a dictionary, an attribute, the result of a method call, or, for numeric attributes, an item in a sequence. This makes it easy to tweak the underlying data model, without having to change the templates.
(Django also supports {% … %} syntax for blocks, but that’s outside the scope of this article. At least this version of it.)
Parsing the Template #
But enough talk; let’s look at some code instead.
To locate the Django variables, you need to parse the template. Since the format is really simple, parsing it is pretty simple too; you can create a token stream simply by splitting the template on “{{” and “}}” markers, and then use an iterator-based parser approach to do the actual parsing.
import re def render(template, context): next = iter(re.split("({{|}})", template)).next data = [] try: token = next() while 1: if token == "{{": # variable data.append(variable(next(), context)) if next() != "}}": raise SyntaxError("missing variable terminator") else: data.append(token) # literal token = next() except StopIteration: pass return data def variable(name, context): return "VARIABLE" # stub!
Running the render function on the sample template gives you the following output.
['\n<h1>', 'VARIABLE', '</h1>\n\n<h2>\n <a href="', 'VARIABLE', '">\n ', 'VARIABLE', '\n </a>\n</h2>\n<p>', 'VARIABLE', '</p>\n']
Note that the render function returns a list of string fragments. Since we’re going to write this to a pipe or a file anyway, there’s really no need to turn this into a single string object. Just use writelines instead of write, and you’re done.
Evaluating Variables #
The next step is to implement the variable function. Django resolves variable names against a given context object, where each part of the name is interpreted as either:
- Dictionary item (object[key]), or
- Attribute (object.key), or
- Method (object.key()), or
- Sequence item (object[int(key)])
in that order.
In other words, when Django sees section.title, it first looks for section[“title”], then for section.title, then for section.title(), and finally for section[int(“title”)]. If none of these work, the result is (usually) set to an empty string.
The following variable function implements the first three; if you need support for sequence items, extending this function should be straightforward.
TEMPLATE_STRING_IF_INVALID = "" def variable(name, context): name = name.strip() if "|" in name: name, filters = name.split("|", 1) obj = context for item in name.split("."): try: obj = obj[item] # dictionary member except (KeyError, AttributeError): try: obj = getattr(obj, item) # or attribute except AttributeError: obj = TEMPLATE_STRING_IF_INVALID break else: if callable(obj): # or method obj = obj() return obj
Applying Filters #
Django also supports variable filters, which are listed after the actual variable definition, and separated from the variables and from each other by vertical bars (|).
The version above ignored all filters. Here’s a somewhat enhanced version that supports a single optional filter (from the set of filters in the FILTERS dictionary). This version still ignores unknown filters, but that’s good enough for my current purposes:
import cgi, string TEMPLATE_STRING_IF_INVALID = "" FILTERS = { "escape": cgi.escape, "upper": string.upper, "lower": string.lower, } def variable(name, context): name = name.strip() if "|" in name: name, filters = name.split("|", 1) else: filters = None obj = context for item in name.split("."): try: obj = obj[item] # dictionary member except (KeyError, AttributeError): try: obj = getattr(obj, item) # or attribute except AttributeError: obj = TEMPLATE_STRING_IF_INVALID break else: if callable(obj): # or method obj = obj() if filters: try: obj = FILTERS[filters](obj) except KeyError: pass return obj
With all this in place, you can quickly parse and render a template with a single call to the render function. The following example introduces two simple classes, and uses a dictionary as the context object.
>>> class SectionObject: ... title = "Section" ... >>> class StoryObject: ... def __init__(self, headline, tease): ... self.headline = headline ... self.tease = tease ... def get_absolute_url(self): ... return "AbsoluteUrl" ... >>> context = dict( ... section=SectionObject(), ... story=StoryObject("Headline", "Tease") ... ) ... >>> import sys >>> sys.stdout.writelines(render(template, context))
<h1>Section Title</h1> <h2> <a href="AbsoluteUrl"> HEADLINE </a> </h2> <p>Tease</p>
Note that the current implementation parses the template and generates the output in a single step. That’s good enough for one-shot generation of static web pages, or for use from CGI scripts, but for heavier use, you may want to cache and reuse the parsed representation in one way or another.