James Tauber wrote a kind of narrative construction of a template language using simple string substitution. It's an interesting way of building up a template language, but I think unnecessarily complex because of some minor details.
The basic structure he uses is this:
class Template:
_template = 'some string with %(sub)s'
def __init__(self, dict):
self.dict = dict
def __str__(self):
return self._template % self
def __getitem__(self, key):
# or some fancier processing...
return self.dict[key].upper()
I think it's easier to not think so much in terms of templates as classes, and to put all the logic in __getitem__ mostly disassociated from templates.
The basic pattern is that of a wrapper:
class Wrapper(object):
def __init__(self, dict):
self.dict = dict
def __getitem__(self, item):
raise NotImplementedError
Used like this:
rendered = 'some string' % SomeWrapper(data_source)
For instance, his piping technique:
class Piper(Wrapper):
def __getitem__(self, item):
parts = item.split("|")
value = self.dict[parts[0]]
for func in parts[1:]:
value = self.dict[func](value)
return value
Then if you use a key of name|upper you will really mean dict['upper'](dict['name']). You actually use it like:
tmpl = "Are you %(name|upper)s?!?"
rendered = tmpl % Piper({'name': 'the keymaster',
'upper': lambda x: x.upper()})
Because of the way string.Template is defined you can't use it with complex key names, but this fixes it (from paste.script.copydir):
class LaxTemplate(string.Template):
# This change of pattern allows for anything in braces, but
# only identifiers outside of braces:
pattern = re.compile(r"""
\$(?:
(?P<escaped>\$) | # Escape sequence of two delimiters
(?P<named>[_a-z][_a-z0-9]*) | # delimiter and a Python identifier
{(?P<braced>.*?)} | # delimiter and a braced identifier
(?P<invalid>) # Other ill-formed delimiter exprs
)
""", re.VERBOSE | re.IGNORECASE)
Then you can do:
tmpl = LaxTemplate('Are you ${name|upper}?!?').substitute(
Piper({'name': 'the keymaster', 'upper': lambda x: x.upper()}))
So, what if you want to HTML quote everything by default?
class HTMLQuote(Wrapper):
def __getitem__(self, item):
value = self.dict[item]
return cgi.escape(str(value), 1)
What if you want to allow arbitrary expressions for keys?
class Eval(Wrapper):
def __getitem__(self, item):
return eval(item, self.dict)
Now you can use a template like Name: %(user.name)s.
What if you want catch exceptions ZPT-style, where you use | to indicate alternatives?
class ZPTCatcher(Wrapper):
def __getitem__(self, item):
alternatives = item.split("|")
for i in alternatives[:-1]:
try:
return self.dict[i]
except (KeyError, AttributeError):
pass
return self.dict[alternatives[-1]]
What if, again ZPT-like, you want to allow different kinds of wrappers based on a prefix? E.g., python:expr for Python syntax, django:value|filter for Django syntax, string:a${b} for a literals with string.Template style substitution.
class Dispatcher(Wrapper):
def __init__(self, dict, **prefix_wrappers):
self.dict = dict
self.prefix_wrappers = prefix_wrappers
def __getitem__(self, item):
if ':' not in item:
wrapper = self.prefix_wrappers['default']
expr = item
else:
prefix, expr = item.split(':', 1)
wrapper = self.prefix_wrappers[prefix]
return wrapper(self.dict)[expr]
class StringTemplate(Wrapper):
def __getitem__(self, item):
tmpl = string.Template(item)
return tmpl.substitute(self.dict)
fancy_dict = Dispatcher(some_dict,
python=Eval,
string=StringTemplate,
django=Piper)
Note that these dictionaries can be used with % or string.Template or anything that uses __getitem__ (and only __getitem__).
This is one reason why any string substitution should allow passing in a dictionary-like object, not just use **kw for passing in dictionaries. This seems to still be a flaw of PEP 3101, which is a expanded string formatting proposal for Python 3. The reason **kw can't work is that these are only very lightly dictionary-like; there is no keys() method. Eval(locals()).keys() would have to enumerate all possible expressions; clearly not feasible. And **kw has to unpack the dictionary (enumerating it) before it can pass it into the function.
you may want to take a look at Templite: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496702
it just lets you embed python code directly into your template.
- for example, this code:
tmpl = "Are you %(name|upper)s?!?"
rendered = tmpl % Piper({'name': 'the keymaster', 'upper': lambda x: x.upper()})
- becomes:
tmpl = Templite("Are you ${emit(name.upper())}$?!?"
rendered = tmpl(name ="the keymaster")
-tomer
I had basically the same idea a few months ago and wrote up a generic object wrapper to help with template substitution. You've inspired me to add it to the Cookbook: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496730