1"""A lightweight python templating engine. Templet version 2 beta. 2 3Slighlty modifed version for MDP (different indentation handling). 4 5 6Supports two templating idioms: 7 1. template functions using @stringfunction and @unicodefunction 8 2. template classes inheriting from StringTemplate and UnicodeTemplate 9 10Each template function is marked with the attribute @stringfunction 11or @unicodefunction. Template functions will be rewritten to expand 12their document string as a template and return the string result. 13For example: 14 15 @stringtemplate 16 def myTemplate(animal, thing): 17 "the $animal jumped over the $thing." 18 19 print myTemplate('cow', 'moon') 20 21The template language understands the following forms: 22 23 $myvar - inserts the value of the variable 'myvar' 24 ${...} - evaluates the expression and inserts the result 25 ${{...}} - executes enclosed code; use 'out.append(text)' to insert text 26 $$ - an escape for a single $ 27 $ (at the end of the line) - a line continuation 28 29Template functions are compiled into code that accumulates a list of 30strings in a local variable 'out', and then returns the concatenation 31of them. If you want do do complicated computation, you can append 32to 'out' directly inside a ${{...}} block. 33 34Another alternative is to use template classes. 35 36Each template class is a subclass of StringTemplate or UnicodeTemplate. 37Template classes should define a class attribute 'template' that 38contains the template code. Also, any class attribute ending with 39'_template' will be compiled into a template method. 40 41Use a template class by instantiating it with a dictionary or 42keyword arguments. Get the expansion by converting the instance 43to a string. For example: 44 45 class MyTemplate(templet.Template): 46 template = "the $animal jumped over the $thing." 47 48 print MyTemplate(animal='cow', thing='moon') 49 50Within a template class, the template language is similar to a template 51function, but 'self.write' should be used to build the string inside 52${{..}} blocks. Also, there is a shorthand for calling template methods: 53 54 $<sub_template> - shorthand for '${{self.sub_template(vars())}}' 55 56This idiom is helpful for decomposing a template and when subclassing. 57 58A longer example: 59 60 import cgi 61 class RecipeTemplate(templet.Template): 62 template = r''' 63 <html><head><title>$dish</title></head> 64 <body> 65 $<header_template> 66 $<body_template> 67 </body></html> 68 ''' 69 header_template = r''' 70 <h1>${cgi.escape(dish)}</h1> 71 ''' 72 body_template = r''' 73 <ol> 74 ${{ 75 for item in ingredients: 76 self.write('<li>', item, '\n') 77 }} 78 </ol> 79 ''' 80 81This template can be expanded as follows: 82 83 print RecipeTemplate(dish='burger', ingredients=['bun', 'beef', 'lettuce']) 84 85And it can be subclassed like this: 86 87 class RecipeWithPriceTemplate(RecipeTemplate): 88 header_template = "<h1>${cgi.escape(dish)} - $$$price</h1>\n" 89 90Templet is by David Bau and was inspired by Tomer Filiba's Templite class. 91For details, see http://davidbau.com/templet 92 93Templet is posted by David Bau under BSD-license terms. 94 95Copyright (c) 2007, David Bau 96All rights reserved. 97 98Redistribution and use in source and binary forms, with or without 99modification, are permitted provided that the following conditions are met: 100 101 1. Redistributions of source code must retain the above copyright notice, 102 this list of conditions and the following disclaimer. 103 104 2. Redistributions in binary form must reproduce the above copyright 105 notice, this list of conditions and the following disclaimer in the 106 documentation and/or other materials provided with the distribution. 107 108 3. Neither the name of Templet nor the names of its contributors may 109 be used to endorse or promote products derived from this software 110 without specific prior written permission. 111 112THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 113ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 114WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 115DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 116ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 117(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 118LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 119ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 120(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 121SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 122""" 123 124import sys, re, inspect 125 126 127class _TemplateBuilder(object): 128 129 __pattern = re.compile(r"""\$( # Directives begin with a $ 130 \$ | # $$ is an escape for $ 131 [^\S\n]*\n | # $\n is a line continuation 132 [_a-z][_a-z0-9]* | # $simple Python identifier 133 \{(?!\{)[^\}]*\} | # ${...} expression to eval 134 \{\{.*?\}\} | # ${{...}} multiline code to exec 135 <[_a-z][_a-z0-9]*> | # $<sub_template> method call 136 )(?:(?:(?<=\}\})|(?<=>))[^\S\n]*\n)? # eat some trailing newlines 137 """, re.IGNORECASE | re.VERBOSE | re.DOTALL) 138 139 def __init__(self, constpat, emitpat, callpat=None): 140 self.constpat, self.emitpat, self.callpat = constpat, emitpat, callpat 141 142 def __realign(self, str, spaces=''): 143 """Removes any leading empty columns of spaces and an initial 144 empty line. 145 146 This is important for embedded Python code. 147 """ 148 lines = str.splitlines() 149 if lines and not lines[0].strip(): del lines[0] 150 lspace = [len(l) - len(l.lstrip()) for l in lines if l.lstrip()] 151 margin = len(lspace) and min(lspace) 152 return '\n'.join((spaces + l[margin:]) for l in lines) 153 154 def build(self, template, filename, s=''): 155 code = [] 156 for i, part in enumerate(self.__pattern.split(template)): 157 if i % 2 == 0: 158 if part: 159 code.append(s + self.constpat % repr(part)) 160 else: 161 if not part or (part.startswith('<') and self.callpat is None): 162 raise SyntaxError('Unescaped $ in ' + filename) 163 elif part.endswith('\n'): 164 continue 165 elif part == '$': 166 code.append(s + self.emitpat % '"$"') 167 elif part.startswith('{{'): 168 code.append(self.__realign(part[2:-2], s)) 169 elif part.startswith('{'): 170 code.append(s + self.emitpat % part[1:-1]) 171 elif part.startswith('<'): 172 code.append(s + self.callpat % part[1:-1]) 173 else: 174 code.append(s + self.emitpat % part) 175 return '\n'.join(code) 176 177 178class _TemplateMetaClass(type): 179 180 __builder = _TemplateBuilder( 181 'self.out.append(%s)', 'self.write(%s)', 'self.%s(vars())') 182 183 def __compile(cls, template, n): 184 globals = sys.modules[cls.__module__].__dict__ 185 if '__file__' not in globals: filename = '<%s %s>' % (cls.__name__, n) 186 else: filename = '%s: <%s %s>' % (globals['__file__'], cls.__name__, n) 187 code = compile(cls.__builder.build(template, filename), filename, 'exec') 188 def expand(self, __dict = None, **kw): 189 if __dict: kw.update([i for i in __dict.iteritems() if i[0] not in kw]) 190 kw['self'] = self 191 exec code in globals, kw 192 return expand 193 194 def __init__(cls, *args): 195 for attr, val in cls.__dict__.items(): 196 if attr == 'template' or attr.endswith('_template'): 197 if isinstance(val, basestring): 198 setattr(cls, attr, cls.__compile(val, attr)) 199 type.__init__(cls, *args) 200 201 202class StringTemplate(object): 203 """A base class for string template classes.""" 204 205 __metaclass__ = _TemplateMetaClass 206 207 def __init__(self, *args, **kw): 208 self.out = [] 209 self.template(*args, **kw) 210 211 def write(self, *args): 212 self.out.extend([str(a) for a in args]) 213 214 def __str__(self): 215 return ''.join(self.out) 216 217 218# The original version of templet called StringTemplate "Template" 219Template = StringTemplate 220 221 222class UnicodeTemplate(object): 223 """A base class for unicode template classes.""" 224 225 __metaclass__ = _TemplateMetaClass 226 227 def __init__(self, *args, **kw): 228 self.out = [] 229 self.template(*args, **kw) 230 231 def write(self, *args): 232 self.out.extend([unicode(a) for a in args]) 233 234 def __unicode__(self): 235 return u''.join(self.out) 236 237 def __str__(self): 238 return unicode(self).encode('utf-8') 239 240 241def _templatefunction(func, listname, stringtype): 242 globals, locals = sys.modules[func.__module__].__dict__, {} 243 if '__file__' not in globals: filename = '<%s>' % func.__name__ 244 else: filename = '%s: <%s>' % (globals['__file__'], func.__name__) 245 builder = _TemplateBuilder('%s.append(%%s)' % listname, 246 '%s.append(%s(%%s))' % (listname, stringtype)) 247 args = inspect.getargspec(func) 248 code = [ 249 'def %s%s:' % (func.__name__, inspect.formatargspec(*args)), 250 ' %s = []' % listname, 251 builder.build(func.__doc__, filename, ' '), 252 ' return "".join(%s)' % listname] 253 code = compile('\n'.join(code), filename, 'exec') 254 exec code in globals, locals 255 return locals[func.__name__] 256 257def stringfunction(func): 258 """Function attribute for string template functions""" 259 return _templatefunction(func, listname='out', stringtype='str') 260 261def unicodefunction(func): 262 """Function attribute for unicode template functions""" 263 return _templatefunction(func, listname='out', stringtype='unicode') 264 265 266# When executed as a script, run some testing code. 267if __name__ == '__main__': 268 ok = True 269 def expect(actual, expected): 270 global ok 271 if expected != actual: 272 print "error - got:\n%s" % repr(actual) 273 ok = False 274 class TestAll(Template): 275 """A test of all the $ forms""" 276 template = r""" 277 Bought: $count ${name}s$ 278 at $$$price. 279 ${{ 280 for i in xrange(count): 281 self.write(TestCalls(vars()), "\n") # inherit all the local $vars 282 }} 283 Total: $$${"%.2f" % (count * price)} 284 """ 285 class TestCalls(Template): 286 """A recursive test""" 287 template = "$name$i ${*[TestCalls(name=name[0], i=n) for n in xrange(i)]}" 288 expect( 289 str(TestAll(count=5, name="template call", price=1.23)), 290 "Bought: 5 template calls at $1.23.\n" 291 "template call0 \n" 292 "template call1 t0 \n" 293 "template call2 t0 t1 t0 \n" 294 "template call3 t0 t1 t0 t2 t0 t1 t0 \n" 295 "template call4 t0 t1 t0 t2 t0 t1 t0 t3 t0 t1 t0 t2 t0 t1 t0 \n" 296 "Total: $6.15\n") 297 class TestBase(Template): 298 template = r""" 299 <head>$<head_template></head> 300 <body>$<body_template></body> 301 """ 302 class TestDerived(TestBase): 303 head_template = "<title>$name</title>" 304 body_template = "${TestAll(vars())}" 305 expect( 306 str(TestDerived(count=4, name="template call", price=2.88)), 307 "<head><title>template call</title></head>\n" 308 "<body>" 309 "Bought: 4 template calls at $2.88.\n" 310 "template call0 \n" 311 "template call1 t0 \n" 312 "template call2 t0 t1 t0 \n" 313 "template call3 t0 t1 t0 t2 t0 t1 t0 \n" 314 "Total: $11.52\n" 315 "</body>\n") 316 class TestUnicode(UnicodeTemplate): 317 template = u""" 318 \N{Greek Small Letter Pi} = $pi 319 """ 320 expect( 321 unicode(TestUnicode(pi = 3.14)), 322 u"\N{Greek Small Letter Pi} = 3.14\n") 323 goterror = False 324 try: 325 class TestError(Template): 326 template = 'Cost of an error: $0' 327 except SyntaxError: 328 goterror = True 329 if not goterror: 330 print 'TestError failed' 331 ok = False 332 @stringfunction 333 def testBasic(name): 334 "Hello $name." 335 expect(testBasic('Henry'), "Hello Henry.") 336 @stringfunction 337 def testReps(a, count=5): r""" 338 ${{ if count == 0: return '' }} 339 $a${testReps(a, count - 1)}""" 340 expect( 341 testReps('foo'), 342 "foofoofoofoofoo") 343 @unicodefunction 344 def testUnicode(count=4): u""" 345 ${{ if not count: return '' }} 346 \N{BLACK STAR}${testUnicode(count - 1)}""" 347 expect( 348 testUnicode(count=10), 349 u"\N{BLACK STAR}" * 10) 350 if ok: print "OK" 351