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