michielovertoom.com

Simple Python template engine • 5 Jan 2011

UPDATE: A more recent version of the source code can be found at github.com/.../ovotemplate.py

An idea for a simple templating engine

I want to do basic template expansion, like this:

Hello {=name}, thanks for your order.

And then feed a 'name' to it, in this way:

vars = { "name": "Joe"}

So that it expands to this:

Hello Joe, thanks for you order.

If "{=name}" occurs multiple times in the template, each instance should be substituted with 'Joe'; and not only the first one encountered.

Reinventing the wheel?

Why did I write this, you ask? Perfectly valid question, because there are already many templating engines out there. I even use a few of them ;-) But for some situations I want to have a template solution which doesn't use embedded Python statements, nor an XML-ish syntax, but does support repeating and conditional blocks. I didn't find an existing one, so I wrote it myself.

In my opinion templates should not contain statements: code belongs in the application. Intermingling code with literal page fragments makes me think of how things were done in the classic ASP days, and stirs up forgotten memories of horrible mashed-together PHP pages. It becomes an unmaintainable mess. Some people object: "So? Why not have code in the template? In the end you are the one looking at them anyway!" But that's not true. I regularly have my templates translated by translators who don't speak any programming language, but understand that something like {product_id} will be filled in later and shouldn't be translated.

On with the feature list! ...

Conditionals

I also want to be able to mark certain parts of the template so they will only appear in the output when a variable is True:

Hello {=name}, thanks for your order. {?ccpayment The sum of {=grandtotal} will be deducted from your creditcard. }

...combined with this data...

vars = { "name": "Joe", "ccpayment": True, "grandtotal": "23.99", }

Result:

Hello Joe, thanks for your order. The sum of 23.99 will be deducted from your creditcard.

If ccpayment would be False, the entire line "The sum of..." would not be displayed.

Repeating groups

And it should do repeating groups, too! For example, to generate the following output:

Hello Joe, thanks for your order. The sum of 23.99 will be deducted from your creditcard. You bought: 2x 'Coders at work' by Peter Seibel = 15.10 1x 'Hackers and painters' by Paul Graham = 3.12 1x 'More Joel on Software' by Joel Spolsky = 7.62

I'd like to use this template:

Hello {=name}, thanks for your order. {?ccpayment The sum of {=grandtotal} will be deducted from your creditcard. } {#purchases {count}x '{title}' by {author} = {total} }

And I'd have to feed it with this data:

vars = { "name": "Joe", "ccpayment": True, "purchases": ( {"count": 2, "title": "Coders at work", "author": "Peter Seibel", "total": 15.10}, {"count": 1, "title": "Hackers and painters", "author": "Paul Graham", "total": 3.12}, {"count": 1, "title": "More Joel on Software", "author": "Joel Spolsky", "total": 7.62}, ) "grandtotal": "23.99", }

Nesting

Nesting would be a great feature. For example, to make a list with sublist like the one below, a nested repeat can be used:

1. Introduction 1.1. Features 1.2. FAQ 1.3. Getting started 2. Usage 2.1. Daily usage 2.2. Exceptional cases

The template would look like this:

{#chapters {=nr}. {=title} {#subchapters {=subnr}. {=subtitle} } }

and the data structure like this:

vars = { "chapters": ( {"nr": "1", "title": "Introduction", "subchapters": ( {"subnr": "1.1", "subtitle": "Features"}, {"subnr": "1.2", "subtitle": "FAQ"}, {"subnr": "1.3", "subtitle": "Getting started"}, ) }, {"nr": "2", "title": "Usage", "subchapters": {"subnr": "2.1", "subtitle": "Daily usage"}, {"subnr": "2.2", "subtitle": "Exceptional cases"}, }, ) }

Source code

# -*- coding: utf8 -*- # ovotemplate.py # # Version 1.1, 5 jan 2010 # # Simple templating class, software by Michiel Overtoom, motoom@xs4all.nl # # {=word} simpele replacement # {?tag ...} conditional # {#tag ...} repeat # {/tag ...} separator # import pprint import re whitechars = re.compile("\s") verbose = False # verbose = True def indent(level): return "| " + " " * level class Container(list): "Generic container" def __init__(self, name=""): self.name = name def __repr__(self): tag = "%s %s" % (self.__class__.__name__, self.name) return "%s: %s" % (tag.strip(), super(Container, self).__repr__()) def render(self, vars, last=False, level=0): if verbose: print "%sContainer.render(vars=%s,last=%s) type(vars)=%s, self.name=%s" % (indent(level),vars,last,type(vars),self.name) output = "" for child in self: if verbose: print "%sContainer.render child %s" % (indent(level), child) output += child.render(vars, last, level+1) return output class Lit(Container): "Container for literal content" def __init__(self, contents=""): super(Lit, self).__init__() self.append(contents) def __repr__(self): return super(Lit, self).__repr__() def render(self, vars, last, level): if verbose: print "%sLit.render(vars=%s,last=%s) type(vars)=%s, self=%s" % (indent(level), vars,last,type(vars),self) return self[0] class Sep(Container): "Container for separator. Same as Lit, but doesn't result in output in the last iteration of a Rep." def __init__(self, name): super(Sep, self).__init__(name) def __repr__(self): return super(Sep, self).__repr__() def render(self, vars, last, level): if verbose: print "%sSep.render(vars=%s) type(vars)=%s, self=%s" % (indent(level), vars,type(vars),self) if last: if verbose: print "%sSep.render last is True, empty string returned" % indent(level) return "" output = "" for child in self: if verbose: print "%sSep.render child %s" % (indent(level), child) output += child.render(vars, last, level+1) return output class Sub(Container): "Container for a variable substitution" def __init__(self, name): super(Sub, self).__init__(name) def __repr__(self): return super(Sub, self).__repr__() def render(self, vars, last, level): if verbose: print "%sSub.render(vars=%s) type(vars)=%s, self.name=%s" % (indent(level), vars,type(vars),self.name) value = vars[self.name] if isinstance(value, (int, float, long)): value = str(value) return value class Cond(Container): "Container for conditional content" def __init__(self, name): super(Cond, self).__init__(name) def __repr__(self): return super(Cond, self).__repr__() def render(self, vars, last, level): if verbose: print "%sCond.render(vars=%s) type(vars)=%s, self.name=%s" % (indent(level), vars,type(vars),self.name) if not vars[self.name]: if verbose: print "%sCond.render cond is False, empty string returned" % indent(level) return "" output = "" for child in self: if verbose: print "%sCond.render child %s" % (indent(level), child) output += child.render(vars, last, level+1) return output class Rep(Container): "Container for repeating content" def __init__(self, name): super(Rep, self).__init__(name) def __repr__(self): return super(Rep, self).__repr__() def render(self, vars, last, level): if verbose: print "%sRep.render(vars=%s) type(vars)=%s, self.name=%s" % (indent(level),vars,type(vars),self.name) output = "" #if not self.name in vars: # raise NameNotFound("A required variable name '%s' was not present in '%r'" % (self.name, vars)) subvars = vars[self.name] # A KeyError here means that a required variable wasn't present. for nr, subvar in enumerate(subvars): if verbose: print "%sRep.render subvar=%s, type(subvar)=%s" % (indent(level),subvar,type(subvar)) for child in self: last = nr == len(subvars)-1 if verbose: print "%sRep.render child %s, last=%s" % (indent(level), child,last) output += child.render(subvar, last, level+1) return output def makecontainer(s): "Return a proper container depending on what type is needed" # TODO: Wel of niet metachars (?,#,=,/) opnemen in naam? name = s[1:].strip() if s.startswith("?"): tmp = Cond(name) elif s.startswith("#"): tmp = Rep(name) elif s.startswith("="): tmp = Sub(name) elif s.startswith("/"): tmp = Sep(name) else: # tmp = Lit(name) raise ValueError("argument '%s' should begin with =, ?, # or /100CASIO" % s) return tmp def splitfirst(s): "Split a string into a first special word, and the rest" if not s: return "", "" if s[0] in "?#=/": parts = whitechars.split(s, 1) if len(parts) < 2: return s, "" else: return parts[0], parts[1] else: return "", s # level 0 is altijd literal text # level 1 en hoger is spul dat met { begint # eerste string moet een van volgende zijn: toegestaan: {?, {#, {= # de rest van de strings op hetzelfde level is literal text def descendingparse(s, i=0, level=0): "Parse a part of the template recursively" if verbose: print "%sEnter descendingparse('%s', %d, %d) s[%d:] = '%s'" % (indent(level), s, i, level, i, s[i:]) collect = "" node = Container() if level == 0 else None while i < len(s): c = s[i] i += 1 if c == "{": if verbose: print "%s{ seen on pos %d, collected text in front of it: '%s' " % (indent(level), i-1, collect) # wat voor de { staat if level == 0: rest = collect else: first, rest = splitfirst(collect) if first: node = makecontainer(first) if rest: node.append(Lit(rest)) i, between = descendingparse(s, i, level + 1) # tussen { en } node.append(between) collect = "" # verzamel opnieuw voor de rest elif c == "}": if level == 0: raise Exception("Too many }") if verbose: print "%s} seen on pos %d, collected text '%s' output before pop" % (indent(level), i-1, collect) # output before pop first, rest = splitfirst(collect) if not node: node = makecontainer(first) if rest: node.append(Lit(rest)) if verbose: print "%sLeave descendingparse, return(%d, %r) s[%d:] = '%s' " % (indent(level), i, node, i, s[i:]) return i, node # pop else: collect += c # end of input string reached if collect: if verbose: print "%sInput exhausted at pos %d, collected text: '%s' " % (indent(level), i, collect) node.append(Lit(collect)) else: if verbose: print "%sInput exhausted at pos %d, no collected text" % (indent(level), i) if verbose: print "%sLeave descendingparse, return(%d, %r) s[%d:] = '%s' " % (indent(level), i, node, i, s[i:]) return i, node class Ovotemplate(object): def __init__(self, s): _, self.root = descendingparse(s) def pprint(self): pprint.pprint(self.root) def render(self, vars): return self.root.render(vars) if __name__ == "__main__": basictests = ( # Basic tests. ("", {}), # pathetisch geval: lege template ("a", {}), # een lettertje ("hallo daar hoe gaat het ermee?", {}), # langere string ("{=status}", {"status": "STATUS"}), # simpele substitutie ("{=status}", {"status": 67.2334}), ("ERVOOR{=status}", {"status": "STATUS"}), ("{=status}ERNA", {"status": "STATUS"}), # twee substituties zonder tekst ertussen: ("{=one}{=two}", {"one": "ONE", "two": "TWO"}), # twee substituties met tekst ertussen: ("{=one}AND{=two}", {"one": "ONE", "two": "TWO"}), # twee substituties met spatie ertussen: ("{=one} {=two}", {"one": "ONE", "two": "TWO"}), # twee substituties met spaties ertussen ("{=one} {=two}", {"one": "ONE", "two": "TWO"}), # twee substituties met tekst en spatie ertussen ("{=one}, {=two}", {"one": "ONE", "two": "TWO"}), # twee substituties met spatie en tekst ertussen ("{=one} ({=two})", {"one": "ONE", "two": "TWO"}), # simpele repetities ("{#cls{=co}}", {"cls": ({"co": "red"}, {"co": "gr"}, {"co": "bl"})}), ("{#cls ({=co})}", {"cls": ({"co": "red"}, {"co": "gr"}, {"co": "bl"})}), ("{#cls {=co}, }", {"cls": ({"co": "red"}, {"co": "gr"}, {"co": "bl"})}), ("{#cls {=co} x }", {"cls": ({"co": "red"}, {"co": "gr"}, {"co": "bl"})}), ("{#cls {=co} _}", {"cls": ({"co": "red"}, {"co": "gr"}, {"co": "bl"})}), # substitutie met tekst ertussen ("well{=here}it{=goes}with{=some}test", {"here": "HERE", "goes": "GOES", "some": "SOME"}), # repeat met variabele als laatste op de regel ("{#blop\n{=you}\n}", dict(blop=(dict(you=123),dict(you=456)))), # simple conditions ("throw a {?condition big }party", {"condition": True}), ("throw a {?condition big }party", {"condition": False}), # repeats ("{#a{=b}{=c}}", {"a": ({"b": 11, "c": 22},)}), # the comma behind the } is significant - without it, the dict wouldn't be placed in a tuple. ("{#a {=b} {=c}}", {"a": [{"b": 33, "c": 44}]}), # or use a list instead of a tuple ("{#a STA{=b}STO BEG{=c}END }", {"a": ({"b": 55, "c": 66},)}), ("{#a {=b} {=c}}", {"a": ({"b": 7.70, "c": 88}, {"b": 99, "c": 1.234567})}), ("{#a {=b} {=c}}", {"a": ()}), ("{#kleuren {=kleur}{/komma , }}", dict(kleuren=(dict(kleur="rood"),dict(kleur="groen"),dict(kleur="blauw")))), # this is an error ("{#a {=b} {=c}}", {}), ) tests = ( ("buy {=count} articles: {#articles {=nam} txt {=pri}, }", {"count": 2, "articles": ({"nam": "Ur", "pri": 1}, {"nam": "Mo", "pri": 2})}), ("dear {=name}, {?market Please get the following groceries: \n \ {#groceries Item: {=item}, {=count} pieces \n }} \ {?deadline Be back before {=time}.}", {"name": "Joe", "market": "True", "count": 5, "groceries": [dict(item="lemon", count=2), dict(item="cookies", count=4)], "deadline": True, "time": "17:30", } ), # condition with repeat ("Contents: {#chapters Chapter {=name}. {#sections Section {=name}. }", {"chapters": [ dict(name="Intro", sections=[dict(name="Foreword"), dict(name="Methodology")]), dict(name="Middle", sections=[dict(name="Measuring"), dict(name="Calculation"), dict(name="Results")]), dict(name="Epilogue", sections=[dict(name="Conclusion")]) ] }), # Nested repeats. ) for tems, temv in basictests+tests: print "Testcase: template = \"%s\", variables = %r" % (tems, temv) tem = Ovotemplate(tems) print tem.pprint() print "Expansion: \"%s\"" % tem.render(temv) print tp = u""" Beste {=naam}, Bedankt voor je bestelling. De volgende artikelen zullen we zo snel mogelijk opsturen: {#levering {=artikel} €{=prijs} * {=aantal} stuks = €{=regtot} } {?backorder De volgende artikelen zijn nog in backorder: {#backartikels {=artikel} €{=prijs} * {=aantal} stuks } } {?buitenland Voor buitenlandse bestellingen wordt {=porto} euro in rekening gebracht.} """ vars = { "naam": "Jan", "levering": ( {"artikel": "Bestekset", "prijs": 35, "aantal": 1, "regtot": 35}, {"artikel": "Schaal", "prijs": 10, "aantal": 2, "regtot": 20}, {"artikel": "Theelepels", "prijs": 1.15, "aantal": 5, "regtot": 5}, ), "backorder": True, "backartikels": ( {"artikel": u"Théédoek", "prijs": 2.50, "aantal": 2}, {"artikel": "Pannenset", "prijs": 79.95, "aantal": 1}, ), "buitenland": False, "porto": "5 euro", } tem = Ovotemplate(tp) tem.pprint() content = tem.render(vars) print content

Comments

Michiel • 21 Dec 2010

This is not complete yet!

Michiel • 25 Apr 2011

However, it is already useful.

Cees Joppe • 14 Aug 2011

Thanx for sharing your clever template Michiel!

Pavel • 26 Nov 2011

GREAT engine! One question: How can you enter a '{' or a '}' into the template for expansion in the out? Thanks!

Bogomil • 20 Mar

I do the following change on rendering all children: output = [] output = [child.render(vars, last, level+1) for child in self] return "".join(output) Now the string operations are much, much faster

Leave a comment

name (required)



content last edited on September 14, 2011, 04:05 - rendered in 9.58 msec