-
-
Save joskid/1625131 to your computer and use it in GitHub Desktop.
Pure python templates using with statement
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
A really stupid python template language inspired by coffeekup, markaby. | |
Do not use this code, it will ruin your day. A byproduct of insomnia. | |
TL;DR | |
----- | |
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') | |
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: | |
<html> | |
<head><title>$title</title></head> | |
<body> | |
<table> | |
#for $client in $clients | |
<tr> | |
<td>$client.surname, $client.firstname</td> | |
<td><a href="mailto:$client.email">$client.email</a></td> | |
</tr> | |
#end for | |
</table> | |
</body> | |
</html> | |
We have '$' and '#' to inline python code and variables. This to me is the | |
wrong way round. I want to write python and not html. I do not want to manage | |
html braces. Tavis Rudd has pointed out that many of the arguments for | |
non python templates are unfounded [2] and I agree with him. This has been | |
further enforced for me by using haml [3] and coffeekup [4], which leaves us in the | |
ridiculous position that the best indent based templating DSLs for html are in | |
non indent based languages (ruby, javascript). | |
This is why we can't have nice things | |
------------------------------------- | |
Coffescript and ruby have some advantage over python for a DSL including the | |
way anonymous blocks are defined and the ability to omit brackets and still get | |
function execution. This means that the most direct python templates, such as | |
lxml builder[5] and breve [6] end up with a nested structure. For example, | |
this is an lxml template: | |
html = E.HTML( | |
E.HEAD( | |
E.LINK(rel="stylesheet", href="great.css", type="text/css"), | |
E.TITLE("Best Page Ever") | |
), | |
E.BODY( | |
E.H1(E.CLASS("heading"), "Top News"), | |
E.P("World News only on this page", style="font-size: 200%"), | |
"Ah, and here's some more text, by the way.", | |
lxml.html.fromstring("<p> and this is a parsed fragment </p>") | |
) | |
) | |
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. | |
The result is the style shown in the TL;DR above. We can also | |
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() | |
The code below also shows template inheritance from python classes. | |
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/ | |
[7] http://spitfire.googlecode.com/svn/trunk/tests/perf/bigtable.py | |
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 context returned by doc has been entered | |
""" | |
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 ItemTemplate(range(10)).render() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment