Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Pure python templates using with statement
"""
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
"""
import lxml
import lxml.html
import lxml.html.builder
import lxml.etree
class TagContext(object):
""" The context manager for an HTML tag """
__slots__ = ['doc', 'node', 'tag']
def __init__(self, tag, doc, *content, **props):
""" create an html tag belonging to doc with given content and properties"""
self.doc = doc
self.tag = tag
def __call__(self, *content, **props):
text = ''
for c in content:
if len(c) and c[0] == '#':
props['id'] = c[1:]
elif len(c) and c[0] == '.':
props['class'] = c[1:]
else:
text += c
text += props.get('text', '')
self.node = getattr(lxml.html.builder, self.tag.upper())(**props)
if self.doc.stack:
self.doc.stack[-1].append(self.node)
if content:
self.node.text = text
self.doc.tail_node = self.node
return self
def __enter__(self):
""" entering the node appends it to the document stack """
self.doc.stack.append(self.node)
self.doc.tail_node = None
def __exit__(self, t, v, tb):
""" exiting the node pops it from the stack """
if len(self.doc.stack) > 1:
self.doc.stack.pop()
self.doc.tail_node = self.node
def each(self, iterable):
for i in iterable:
with self():
yield self, i
def set(self, key, value):
self.node.set(key, value)
class Lazydec(object):
""" Horrible decorator to allow the with statement to omit
brackets by tracking if context returned by doc has been entered
"""
__slots__ = ['x']
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 """
Context = TagContext
def __init__(self, *args, **kws):
self.stack = []
self.tail_node = None # this node is set if text needs to be put in its tail
def __getattr__(self, name):
""" Override getattr for quick tag access
This means Doc.html returns a tag
"""
return self.Context(name, self)
def text(self, text):
""" append raw text """
if self.tail_node is not None:
self.tail_node.tail = (self.tail_node.tail or '') + text
else:
self.stack[-1].text = (self.stack[-1].text or '') + text
def fragment(self, html):
""" append a html fragment (must be a single element) """
try:
self.stack[-1].append(lxml.html.fragment_fromstring(html))
except lxml.etree.ParserError:
els = lxml.html.fragments_fromstring(html)
for e in els:
self.stack[-1].append(e)
def to_string(self, pretty_print=True):
""" convert this document to string """
return lxml.html.tostring(self.stack[0],
pretty_print=pretty_print,
doctype='<!DOCTYPE html>')
class BootstrapContext(TagContext):
def _add_class(self, props, cls):
if 'class' not in props:
props['class'] = cls
else:
props['class'] = props['class'] + ' ' + cls
def __call__(self, *content, **props):
# support span and offset
for gridcls in ['span', 'offset']:
if gridcls in props:
self._add_class(props, gridcls + str(props[gridcls]))
props.pop(gridcls)
return TagContext.__call__(self, *content, **props)
class Bootstrap(Doc):
Context = BootstrapContext
if __name__ == '__main__':
def example_template(items):
""" template function example"""
d = Doc()
p = d.html()
print p.__class__
with d.html():
d.fragment('<h1>no tail</h1>')
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')
d.text('hi')
d.div('.foo', '#bar', 'inside el')
d.text('bye')
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 ('#header'):
self.header(d)
with d.div ('#content'):
self.content(d)
with d.div ('#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 li, i in d.li.each(self.items):
li.set('id', str(i))
d.text('the cake is a li')
d.a (str(i), href=str(i) + '.html')
def bootstrap_example():
d = Bootstrap()
with d.html():
d.div('#main', span=8, offset=3)
return d.to_string()
print example_template(range(10))
print
print ItemTemplate(range(10)).render()
print
print bootstrap_example()
@MagnusOxlund

This comment has been minimized.

Copy link

commented Oct 26, 2017

What's so stupid about it?

@DrecDroid

This comment has been minimized.

Copy link

commented Oct 26, 2017

It looks really good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.