""" stemp: a simple string templating language - (c) Guido Wesdorp 2009 Basically all that stemp.stemplate does is introduce an additional string formatting construct to Python, which allows dealing with blocks. It allows blocks of text to be rendered conditionally, or allows them to be repeated. A simple example renders the word 'baz' in the string 'foo bar baz' depending on the value of an item in a provided context dict: >>> t = 'foo %(cond)?bbar %(cond)>bbaz' >>> s = str(stemplate({'cond': False})) 'foo baz' >>> s = str(stemplate({'cond': True})) 'foo bar baz' As you can see, if 'cond' is False, the word 'baz' is not rendered, while if it's True, 'baz' is there. For the opening/closing 'tag' syntax, I decided to use the format:: %()b where '' is to be replaced by the key that provides the condition or content of the block, and is one character explaining what the tag is for. The action can be '?' for opening conditional blocks, '~' for negated conditions, '*' for opening loops, and '>' for closing any type of block. A more elaborate example of a loop, in this case we loop through some product data in order to create some emails:: >>> t = '''\ Subject: Don't be the laughing stock anymore! To: %(to)s From: %(to)s Our store delivers the following products to spark up your love-life: %(product)*b %(name)s - %(price)s %(description)s%(product)>b See our website at %(url)s for more information! ''' >>> s = stemplate(t) >>> for poor_sod in our_huge_email_list: ... data = product_data.copy() ... data.update({'to': poor_sod, ... 'url': http://resgradsefc.ru/'}) ... send_email(poor_sod, [poor_sod], s.process(data)) A few points: * currently error reporting is not optimal, since we use simple regular expressions rather than a full parser we can not provide the user with line numbers etc. * bad nesting is reported, but improperly formed tags are not detected and may result in a rather cryptic error - basically if you get an exception in the line:: t = t % data you may want to check the format of your tags * white-space can be hard to deal with properly For questions, remarks, etc. mail johnny@johnnydebris.net. """ import re class stemplate(object): def __init__(self, template, data=None, charset='ascii'): if isinstance(template, str): template = unicode(template, charset) self.template = template self.data = data self.charset = charset self._cache = None def __str__(self): if self._cache is not None: return self._cache.encode(self.charset) self._cache = ret = self.process(self.data) return ret.encode(self.charset) def __unicode__(self): if self._cache is not None: return self._cache self._cache = ret = self.process(self.data) return ret def process(self, data): return self._process_template(self.template, data) def _process_template(self, t, data): if data is None: raise TypeError('no context data provided') t = self._process_blocks(t, data) if isinstance(data, dict): for k, v in data.iteritems(): if isinstance(v, str): data[k] = unicode(v, self.charset) t = t % data return t _reg_block = re.compile( r'.*(%\(([^)]+)\)([*?~<])b(.*?)%\(([^)]+)\)>b)', re.U | re.S) _reg_leftover = re.compile(r'%\(([^)]+)\)(.)b') def _process_blocks(self, t, data): while True: match = self._reg_block.search(t) if not match: break key = match.group(2) ckey = match.group(5) if key != ckey: raise SyntaxError( 'opening block key %s does not match nearest ' 'closing key %s' % (key, ckey)) action = match.group(3) block = match.group(4) value = data[key] handler = { '<': self._process_block, '?': self._process_condition, '~': self._process_negation, '*': self._process_loop, }.get(action) t = t.replace(match.group(1), handler(block, key, value)) match_leftover = self._reg_leftover.search(t) if match_leftover: raise TypeError( 'unexpected tag %s of type %s' % ( match_leftover.group(1), match_leftover.group(2))) return t def _process_block(self, block, key, value): if not isinstance(value, dict): raise TypeError( 'expected dict for block interpolation %s' % (key,)) return self._process_template(block, value) def _process_condition(self, block, key, value): if value: return block return '' def _process_negation(self, block, key, value): if not value: return block return '' def _process_loop(self, block, key, value): ret = [] if type(value) in (str, unicode): raise TypeError('%s: value must be non-string iterable' % (key,)) try: for x in value: if not isinstance(x, dict): raise TypeError( 'can only interpolate dictionaries using *b') processed = self._process_template(block, x) ret.append(processed) except TypeError, e: raise TypeError('%s: %s' % (key, e)) return ''.join(ret)