Skip to content

Instantly share code, notes, and snippets.

@scott2b
Last active December 1, 2022 14:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scott2b/3a538eddcfed77c0f686efe3ffc0ebb4 to your computer and use it in GitHub Desktop.
Save scott2b/3a538eddcfed77c0f686efe3ffc0ebb4 to your computer and use it in GitHub Desktop.
A simple templating engine that handles nested context
import re
from functools import partial
L_BR = "__LBR__"
R_BR = "__RBR__"
class SimpleTemplate:
"""A super simple templating engine that will handle arbitrarily nested context.
Templated properties are marked by `{ ... }`. Attributes of the context are dot-delimited.
Because of the simple regex approach, odds and ends literal braces will render. However, if
you are trying to render something like a literal `{ some.property }`, you will need to
double up the braces: `{{ some.property }}` to render as expected.
>>> t = SimpleTemplate("I spent all my {currency} on {thing}")
>>> t.render_strict(**{"foo": "bar"})
Traceback (most recent call last):
...
KeyError: 'currency'
>>> t.render(currency="buttons", thing="an old pack mule")
'I spent all my buttons on an old pack mule'
>>> # This is a bit quirky: the initial {{}} construct is replaced by private tags
>>> # that end up treated as properties because they are in { }. Fails in strict
>>> # mode. But in non-strict render just replaces `{ {{}} }` with an empty string.
>>> t = SimpleTemplate("{ {{}} }tore out the { {{}} }bucket from a red Corvette{ {{}} }")
>>> t.render_strict(foo="bar")
Traceback (most recent call last):
...
KeyError: '__LBR____RBR__'
>>> t.render(foo="bar")
'tore out the bucket from a red Corvette'
>>> t = SimpleTemplate("}{ }filled me a {{satchel}} full { of old pig corn { }}")
>>> t.render(satchel="shouldn't match because of double-braces")
'}filled me a {satchel} full { of old pig corn { }'
>>> t.render_strict(satchel="shouldn't match because of double-braces")
Traceback (most recent call last):
...
KeyError: ''
>>> t = SimpleTemplate("how to do {{{{literal}}}} {{{{ double braces. }}}}")
>>> t.render(**{})
'how to do {{literal}} {{ double braces. }}'
>>> t = SimpleTemplate("whittle you into {one} Black crow, { two.three } shells from a {two.four.five}-ought-{two.four.six.seven}")
>>> d = { "one": "kindlin'", "two": { "three": 16, "four": { "five": "thirty", "six": { "seven": "six" }}}}
>>> t.render(**d)
"whittle you into kindlin' Black crow, 16 shells from a thirty-ought-six"
"""
regex = re.compile("({[^{}]*?})", re.I | re.S | re.M)
def __init__(self, s):
self.template = s.replace("{{", L_BR).replace("}}", R_BR)
class Context(dict):
__slots__ = ()
__setattr__ = dict.__setitem__
def __getattr__(self, a):
a, *r = a.split(".")
i = self.__getitem__(a)
if isinstance(i, dict):
i = SimpleTemplate.Context(i)
if r:
return getattr(i, ".".join(r))
return i
def replacer(self, obj, context, ignore_missing):
orig = obj.group(0)
check = orig.strip("{}")
if len(check) < len(orig) - 2:
return orig.replace("{{", "{").replace("}}", "}")
try:
return str(getattr(context, check.strip()))
except KeyError:
if ignore_missing:
return ""
raise
def _render(self, __ignore__missing__attrs__=True, **context):
context = SimpleTemplate.Context(context)
r = re.sub(
self.regex,
partial(
self.replacer,
context=context,
ignore_missing=__ignore__missing__attrs__,
),
self.template,
)
return r.replace(L_BR, "{").replace(R_BR, "}")
def render(self, **context):
return self._render(__ignore__missing__attrs__=True, **context)
def render_strict(self, **context):
return self._render(__ignore__missing__attrs__=False, **context)
if __name__ == "__main__":
import doctest
doctest.testmod()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment