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