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""" 123from __future__ import print_function 124from builtins import str 125from past.builtins import basestring 126from builtins import object 127 128import sys, re, inspect 129from future.utils import with_metaclass 130 131 132class _TemplateBuilder(object): 133 134 __pattern = re.compile(r"""\$( # Directives begin with a $ 135 \$ | # $$ is an escape for $ 136 [^\S\n]*\n | # $\n is a line continuation 137 [_a-z][_a-z0-9]* | # $simple Python identifier 138 \{(?!\{)[^\}]*\} | # ${...} expression to eval 139 \{\{.*?\}\} | # ${{...}} multiline code to exec 140 <[_a-z][_a-z0-9]*> | # $<sub_template> method call 141 )(?:(?:(?<=\}\})|(?<=>))[^\S\n]*\n)? # eat some trailing newlines 142 """, re.IGNORECASE | re.VERBOSE | re.DOTALL) 143 144 def __init__(self, constpat, emitpat, callpat=None): 145 self.constpat, self.emitpat, self.callpat = constpat, emitpat, callpat 146 147 def __realign(self, str, spaces=''): 148 """Removes any leading empty columns of spaces and an initial 149 empty line. 150 151 This is important for embedded Python code. 152 """ 153 lines = str.splitlines() 154 if lines and not lines[0].strip(): del lines[0] 155 lspace = [len(l) - len(l.lstrip()) for l in lines if l.lstrip()] 156 margin = len(lspace) and min(lspace) 157 return '\n'.join((spaces + l[margin:]) for l in lines) 158 159 def build(self, template, filename, s=''): 160 code = [] 161 for i, part in enumerate(self.__pattern.split(template)): 162 if i % 2 == 0: 163 if part: 164 code.append(s + self.constpat % repr(part)) 165 else: 166 if not part or (part.startswith('<') and self.callpat is None): 167 raise SyntaxError('Unescaped $ in ' + filename) 168 elif part.endswith('\n'): 169 continue 170 elif part == '$': 171 code.append(s + self.emitpat % '"$"') 172 elif part.startswith('{{'): 173 code.append(self.__realign(part[2:-2], s)) 174 elif part.startswith('{'): 175 code.append(s + self.emitpat % part[1:-1]) 176 elif part.startswith('<'): 177 code.append(s + self.callpat % part[1:-1]) 178 else: 179 code.append(s + self.emitpat % part) 180 return '\n'.join(code) 181 182def _exec(code, globals, locals): 183 exec(code, globals, locals) 184 185class _TemplateMetaClass(type): 186 187 __builder = _TemplateBuilder( 188 'self.out.append(%s)', 'self.write(%s)', 'self.%s(vars())') 189 190 def __compile(cls, template, n): 191 globals = sys.modules[cls.__module__].__dict__ 192 if '__file__' not in globals: filename = '<%s %s>' % (cls.__name__, n) 193 else: filename = '%s: <%s %s>' % (globals['__file__'], cls.__name__, n) 194 code = compile(cls.__builder.build(template, filename), filename, 'exec') 195 def expand(self, __dict = None, **kw): 196 if __dict: kw.update([i for i in __dict.items() if i[0] not in kw]) 197 kw['self'] = self 198 _exec(code, globals, kw) 199 return expand 200 201 def __init__(cls, *args): 202 for attr, val in list(cls.__dict__.items()): 203 if attr == 'template' or attr.endswith('_template'): 204 if isinstance(val, basestring): 205 setattr(cls, attr, cls.__compile(val, attr)) 206 type.__init__(cls, *args) 207 208 209class StringTemplate(with_metaclass(_TemplateMetaClass, object)): 210 """A base class for string template classes.""" 211 212 def __init__(self, *args, **kw): 213 self.out = [] 214 self.template(*args, **kw) 215 216 def write(self, *args): 217 self.out.extend([str(a) for a in args]) 218 219 def __str__(self): 220 return ''.join(self.out) 221 222 223# The original version of templet called StringTemplate "Template" 224Template = StringTemplate 225 226 227class UnicodeTemplate(with_metaclass(_TemplateMetaClass, object)): 228 """A base class for unicode template classes.""" 229 230 def __init__(self, *args, **kw): 231 self.out = [] 232 self.template(*args, **kw) 233 234 def write(self, *args): 235 self.out.extend([str(a) for a in args]) 236 237 def __unicode__(self): 238 return u''.join(self.out) 239 240 def __str__(self): 241 return str(self).encode('utf-8') 242 243 244def _templatefunction(func, listname, stringtype): 245 globals, locals = sys.modules[func.__module__].__dict__, {} 246 if '__file__' not in globals: filename = '<%s>' % func.__name__ 247 else: filename = '%s: <%s>' % (globals['__file__'], func.__name__) 248 builder = _TemplateBuilder('%s.append(%%s)' % listname, 249 '%s.append(%s(%%s))' % (listname, stringtype)) 250 args = inspect.getargspec(func) 251 code = [ 252 'def %s%s:' % (func.__name__, inspect.formatargspec(*args)), 253 ' %s = []' % listname, 254 builder.build(func.__doc__, filename, ' '), 255 ' return "".join(%s)' % listname] 256 code = compile('\n'.join(code), filename, 'exec') 257 exec(code, globals, locals) 258 return locals[func.__name__] 259 260def stringfunction(func): 261 """Function attribute for string template functions""" 262 return _templatefunction(func, listname='out', stringtype='str') 263 264def unicodefunction(func): 265 """Function attribute for unicode template functions""" 266 return _templatefunction(func, listname='out', stringtype='unicode') 267 268 269# When executed as a script, run some testing code. 270if __name__ == '__main__': 271 ok = True 272 def expect(actual, expected): 273 global ok 274 if expected != actual: 275 print("error - got:\n%s" % repr(actual)) 276 ok = False 277 class TestAll(Template): 278 """A test of all the $ forms""" 279 template = r""" 280 Bought: $count ${name}s$ 281 at $$$price. 282 ${{ 283 for i in xrange(count): 284 self.write(TestCalls(vars()), "\n") # inherit all the local $vars 285 }} 286 Total: $$${"%.2f" % (count * price)} 287 """ 288 class TestCalls(Template): 289 """A recursive test""" 290 template = "$name$i ${*[TestCalls(name=name[0], i=n) for n in xrange(i)]}" 291 expect( 292 str(TestAll(count=5, name="template call", price=1.23)), 293 "Bought: 5 template calls at $1.23.\n" 294 "template call0 \n" 295 "template call1 t0 \n" 296 "template call2 t0 t1 t0 \n" 297 "template call3 t0 t1 t0 t2 t0 t1 t0 \n" 298 "template call4 t0 t1 t0 t2 t0 t1 t0 t3 t0 t1 t0 t2 t0 t1 t0 \n" 299 "Total: $6.15\n") 300 class TestBase(Template): 301 template = r""" 302 <head>$<head_template></head> 303 <body>$<body_template></body> 304 """ 305 class TestDerived(TestBase): 306 head_template = "<title>$name</title>" 307 body_template = "${TestAll(vars())}" 308 expect( 309 str(TestDerived(count=4, name="template call", price=2.88)), 310 "<head><title>template call</title></head>\n" 311 "<body>" 312 "Bought: 4 template calls at $2.88.\n" 313 "template call0 \n" 314 "template call1 t0 \n" 315 "template call2 t0 t1 t0 \n" 316 "template call3 t0 t1 t0 t2 t0 t1 t0 \n" 317 "Total: $11.52\n" 318 "</body>\n") 319 class TestUnicode(UnicodeTemplate): 320 template = u""" 321 \N{Greek Small Letter Pi} = $pi 322 """ 323 expect( 324 str(TestUnicode(pi = 3.14)), 325 u"\N{Greek Small Letter Pi} = 3.14\n") 326 goterror = False 327 try: 328 class TestError(Template): 329 template = 'Cost of an error: $0' 330 except SyntaxError: 331 goterror = True 332 if not goterror: 333 print('TestError failed') 334 ok = False 335 @stringfunction 336 def testBasic(name): 337 "Hello $name." 338 expect(testBasic('Henry'), "Hello Henry.") 339 @stringfunction 340 def testReps(a, count=5): r""" 341 ${{ if count == 0: return '' }} 342 $a${testReps(a, count - 1)}""" 343 expect( 344 testReps('foo'), 345 "foofoofoofoofoo") 346 @unicodefunction 347 def testUnicode(count=4): u""" 348 ${{ if not count: return '' }} 349 \N{BLACK STAR}${testUnicode(count - 1)}""" 350 expect( 351 testUnicode(count=10), 352 u"\N{BLACK STAR}" * 10) 353 if ok: print("OK") 354