""" A really stupid python template language inspired by coffeekup, markaby. Do not use this code, it will ruin your day. A byproduct of insomnia. Motivation ---------- Python templating has always been a problem for me. You normally have to write in your target language (i.e. HTML) with some horrible syntax for inlining python. For example, this is a Cheetah [1] template:
| $client.surname, $client.firstname | $client.email |
and this is a parsed fragment
") ) ) Now, this code is python, but logic expressed this way still requires management of parens and is not indent based - so is not really 'pythonic'. It also cannot really use conditionals and loops beyond ternarys and list comprehensions. So I wondered if we could hook up a bit of magic using some other method. So which way to go? Function definitions would require too much magic, so it seems that the best way is the _with_ statement. This module defines a template language that allows us to do: d = Doc() with d.html: with d.head: d.title ('example page') d.link (rel='stylesheet', href='/style.css', type='text/css') with d.body (style='foo'): d.a ('other stuff on another page', href='/other.html') d.p ('stuff on this page') We can wrap this in a template method to use python conditional logic based on supplied data: def example_template(items): d = Doc() with d.html: with d.head: d.title ('other stuff on this page') d.link (rel='stylesheet', href='/style.css', type='text/css') with d.body (style='foo'): d.a ('other stuff on another page', href='/other.html') d.p ('stuff on this page') with d.ul: for i in items: with d.li: d.a (str(i), href=str(i) + '.html') return d.to_string() This is currently based on the lxml builder, and so it is slightly slower than the elementree benchmarks [7] (which is pretty slow). The lazy decorator that allows us to miss out brackets on with statements is probably going to make you cry. Still this seems like a reasonable template language in about 40 lines of code. [1] http://www.cheetahtemplate.org/examples.html [2] https://bitbucket.org/tavisrudd/throw-out-your-templates/src/98c5afba7f35/throw_out_your_templates.py [3] http://haml-lang.com/ [4] http://coffeekup.org/ [5] http://lxml.de/dev/api/lxml.builder.ElementMaker-class.html [6] http://breve.twisty-industries.com/ Comments, abuse, etc to twitter @casualbon or casbon (at) gmail.com """ from lxml.html import builder as E import lxml class TagContext(object): """ The context manager for an HTML tag """ def __init__(self, tag, doc, *content, **props): """ create an html tag belonging to doc with given content and properties""" self.tag = tag self.doc = doc self.content = content self.props = props self.stack = self.doc.stack self.node = self.make_node() def make_node(self): """ create an lxml element and append it to the node at the top of the document tack """ node = getattr(E, self.tag.upper())(*self.content, **self.props) if self.stack: self.stack[-1].append(node) return node def __enter__(self): """ entering the node appends it to the document stack """ self.stack.append(self.node) def __exit__(self, t, v, tb): """ exiting the node pops it from the stack """ if len(self.stack) > 1: self.stack.pop() class Lazydec(object): """ Horrible decorator to allow the with statement to omit brackets by tracking if callable returned by doc has been called """ def __init__(self, x): self.x = x def __call__(self, *args, **kws): return self.x(*args, **kws) def __enter__(self): if self.x.__name__ == 'tagcallable': self.x = self.x() return self.x.__enter__() def __exit__(self, *args): return self.x.__exit__(*args) class Doc(object): """ A document manages a stack, the head of which is the current context node """ def __init__(self, *args, **kws): self.stack = [] def __getattr__(self, attr): """ Override getattr for quick tag access This means Doc.html returns a tag """ # TODO: incomplete list of tags if attr in ['html', 'head', 'body', 'a', 'p', 'ul', 'li', 'div', 'table', 'tr', 'td', 'link', 'title', 'span']: return self.make_tag(attr) else: return object.getattr(self, attr) def to_string(self, pretty_print=True): """ convert this document to string """ return lxml.html.tostring(self.stack[0], pretty_print=pretty_print) def make_tag(self, name): """ create a tag by creating a TagContext and then decorating it with the lazy decorator""" def tagcallable(*content, **props): return TagContext(name, self, *content, **props) f = Lazydec(tagcallable) return f if __name__ == '__main__': def example_template(items): """ template function example""" d = Doc() with d.html: with d.head: d.title ('other stuff on this page') d.link (rel='stylesheet', href='/style.css', type='text/css') with d.body (style='foo'): d.a ('other stuff on another page', href='/other.html') d.p ('stuff on this page') with d.ul: for i in items: with d.li: d.a (str(i), href=str(i) + '.html') return d.to_string() class BaseTemplate(object): """ template inheritance example """ title = 'base' def render(self): d = Doc() with d.html: with d.head: d.title (self.title) d.link (rel='stylesheet', href='/style.css', type='text/css') with d.body (style='foo'): with d.div (id='header'): self.header(d) with d.div (id='content'): self.content(d) with d.div (id='footer'): self.footer(d) return d.to_string() def header(self, d): pass def content(self, d): pass def footer(self, d): d.span('Never read this content') class ItemTemplate(BaseTemplate): title = 'item view' def __init__(self, items): self.items = items def header(self, d): d.p('view of %s items' % len(self.items)) def content(self, d): with d.ul: for i in self.items: with d.li: d.a (str(i), href=str(i) + '.html') print example_template(range(10)) print print ItemTemplate(range(10)).render()