In someone else’s project (which they’ll doubtless tell you about themselves when it’s done) I needed a tiny Python templating engine. That is: I wanted to be able to say, here is a template string, please substitute a bunch of variables into it. Now, Python already does this, in about thirty different ways, and str.format
or string.Template
do most of it as built-in.
str.format
works like this:
"My name is {name} and I am {age} years old".format(name="Stuart", age=43)
and string.Template
like this:
t=string.Template(
"My name is $name and I am $age years old"
).safe_substitute(name="Stuart", age=43)
Both of which are pretty OK.
However, what they’re missing is loops; having more than one of a thing in your template, and looping over a list, substituting it each time. Every even fractionally-more-featureful templating system has this, whether Mustache or Jinja or whatever, of course, but I didn’t want another dependency. All I needed was str.format
but with loops. So, I thought, I’ll write one, in about four lines of code, so I can just drop the function in to my Python file and then I’m good.
def LoopTemplate(s, ctx):
def loophandler(m):
md = m.groupdict()
return "".join([LoopTemplate(md["content"], val)
for val in ctx[md["var"]]])
return re.sub(r"\{loop (?P<var>[^}]+)\}(?P<content>.*?)\{endloop\}",
loophandler, s, flags=re.DOTALL).format(**ctx)
And lo, twas so. So I can now do
LoopTemplate(
"I am {name} and my imps' names are: {loop imps}{name}{endloop}",
{
"name": "Stuart",
"imps": [
{"name": "Pyweazle"}, {"name": "Grimthacket"}, {"name": "Hardebon"}
]
}
)
and it all works. Not revolutionary, of course, but I was mildly pleased with myself.
Much internal debate about whether loophandler()
should have been a lambda
, but I eventually decided it was more confusing that way, on the grounds that it was confusing me and I knew what it was meant to be doing.
A brief explanation: re.sub
lets you pass a function as the thing to replace with, rather than just a string. So we find all examples of {loop something}...{endloop}
in the passed string, look up something
in the “context”, or the dict of substitution variables you passed to LoopTemplate
, and then we call LoopTemplate
again, once per item in something
(which is expected to be a list), and pass it the ...
as its string and the next item in something
as its context. So it all works. Of course, there’s no error handling or anything — if something
isn’t present in the context, or if it’s not a list, or if you stray in any other way from the path of righteousness, it’ll incomprehensibly blow up. So don’t do that.