Last active
December 1, 2022 14:43
-
-
Save scott2b/3a538eddcfed77c0f686efe3ffc0ebb4 to your computer and use it in GitHub Desktop.
A simple templating engine that handles nested context
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
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