"""A lightweight python templating engine. Templet version 3.3
Lightweight templating idiom using @stringfunction and @unicodefunction.
Each template function is marked with the attribute @stringfunction
or @unicodefunction. Template functions will be rewritten to expand
their document string as a template and return the string result.
For example:
from templet import stringfunction
@stringfunction
def myTemplate(animal, body):
"the $animal jumped over the $body."
print myTemplate('cow', 'moon')
The template language understands the following forms:
$myvar - inserts the value of the variable 'myvar'
${...} - evaluates the expression and inserts the result
${[...]} - evaluates the list comprehension and inserts all the results
${{...}} - executes enclosed code; use 'out.append(text)' to insert text
In addition the following special codes are recognized:
$$ - an escape for a single $
$ (at the end of the line) - a line continuation
$( $. - translates directly to $( and $. so jquery does not need escaping
$/ $' $" - also passed through so the end of a regex does not need escaping
Template functions are compiled into code that accumulates a list of
strings in a local variable 'out', and then returns the concatenation
of them. If you want do do complicated computation, you can append
to the 'out' variable directly inside a ${{...}} block, for example:
@stringfunction
def myrow(name, values):
'''
| $name | ${{
for val in values:
out.append(string(val))
}} |
'''
Generated code is arranged so that error line numbers are reported as
accurately as possible.
Templet is by David Bau and was inspired by Tomer Filiba's Templite class.
For details, see http://davidbau.com/templet
Templet is posted by David Bau under BSD-license terms.
Copyright (c) 2012, David Bau
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of Templet nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import sys, re, inspect
class _TemplateBuilder(object):
__pattern = re.compile(r"""\$ # Directives begin with a $
(?![.(/'"])( # $. $( $/ $' $" do not require escape
\$ | # $$ is an escape for $
[^\S\n]*\n | # $\n is a line continuation
[_a-z][_a-z0-9]* | # $simple Python identifier
\{(?![[{])[^\}]*\} | # ${...} expression to eval
\{\[.*?\]\} | # ${[...]} list comprehension to eval
\{\{.*?\}\} | # ${{...}} multiline code to exec
)((?<=\}\})[^\S\n]*\n|) # eat trailing newline after }}
""", re.IGNORECASE | re.VERBOSE | re.DOTALL)
def __init__(s, *args):
s.defn, s.start, s.constpat, s.emitpat, s.listpat, s.finish = args
def __realign(self, str, spaces=''):
"""Removes any leading empty columns of spaces and an initial empty line"""
lines = str.splitlines()
if lines and not lines[0].strip(): del lines[0]
lspace = [len(l) - len(l.lstrip()) for l in lines if l.lstrip()]
margin = len(lspace) and min(lspace)
return '\n'.join((spaces + l[margin:]) for l in lines)
def __addcode(self, line, lineno, simple):
offset = lineno - self.extralines - len(self.code)
if offset <= 0 and simple and self.simple and self.code:
self.code[-1] += ';' + line
else:
self.code.append('\n' * (offset - 1) + line);
self.extralines += max(0, offset - 1)
self.extralines += line.count('\n')
self.simple = simple
def build(self, template, filename, lineno, docline):
self.code = ['\n' * (lineno - 1) + self.defn, ' ' + self.start]
self.extralines, self.simple = max(0, lineno - 1), True
lineno += docline + (re.match(r'\s*\n', template) and 1 or 0)
for i, part in enumerate(self.__pattern.split(self.__realign(template))):
if i % 3 == 0 and part:
self.__addcode(' ' + self.constpat % repr(part), lineno, True)
elif i % 3 == 1:
if not part:
raise SyntaxError('Unescaped $ in %s:%d' % (filename, lineno))
elif part == '$':
self.__addcode(' ' + self.constpat % '"%s"' % part, lineno, True)
elif part.startswith('{{'):
self.__addcode(self.__realign(part[2:-2], ' '),
lineno + (re.match(r'\{\{\s*\n', part) and 1 or 0), False)
elif part.startswith('{['):
self.__addcode(' ' + self.listpat % part[2:-2], lineno, True)
elif part.startswith('{'):
self.__addcode(' ' + self.emitpat % part[1:-1], lineno, True)
elif not part.endswith('\n'):
self.__addcode(' ' + self.emitpat % part, lineno, True)
lineno += part.count('\n')
self.code.append(' ' + self.finish)
return '\n'.join(self.code)
def _templatefunction(func, listname, stringtype):
globals, locals = sys.modules[func.__module__].__dict__, {}
filename, lineno = func.func_code.co_filename, func.func_code.co_firstlineno
if func.__doc__ is None:
raise SyntaxError('No template string at %s:%d' % (filename, lineno))
try: # scan source code to find the docstring line number (2 if not found)
docline, (source, _) = 2, inspect.getsourcelines(func)
for lno, line in enumerate(source):
if re.match('(?:|[^#]*:)\\s*[ru]?[\'"]', line): docline = lno; break
except:
docline = 2
args = inspect.getargspec(func)
builder = _TemplateBuilder(
'def %s%s:' % (func.__name__, inspect.formatargspec(*args)),
'%s = []' % listname,
'%s.append(%%s)' % listname,
'%s.append(%s(%%s))' % (listname, stringtype),
'%s.extend(map(%s, [%%s]))' % (listname, stringtype),
'return "".join(%s)' % listname)
code_str = builder.build(func.__doc__, filename, lineno, docline)
code = compile(code_str, filename, 'exec')
exec code in globals, locals
return locals[func.__name__]
def stringfunction(func):
"""Function attribute for string template functions"""
return _templatefunction(func, listname='out', stringtype='str')
def unicodefunction(func):
"""Function attribute for unicode template functions"""
return _templatefunction(func, listname='out', stringtype='unicode')
##############################################################################
# When executed as a script, run some testing code.
if __name__ == '__main__':
ok = True
def expect(actual, expected):
global ok
if expected != actual:
print "error - expect: %s, got:\n%s" % (repr(expected), repr(actual))
ok = False
@stringfunction
def testBasic(name):
"Hello $name."
expect(testBasic('Henry'), "Hello Henry.")
@stringfunction
def testReps(a, count=5): r"""
${{ if count == 0: return '' }}
$a${testReps(a, count - 1)}"""
expect(
testReps('foo'),
"foofoofoofoofoo")
@stringfunction
def testList(a): r"""
${[testBasic(x) for x in a]}"""
expect(
testList(['David', 'Kevin']),
"Hello David.Hello Kevin.")
@unicodefunction
def testUnicode(count=4): u"""
${{ if not count: return '' }}
\N{BLACK STAR}${testUnicode(count - 1)}"""
expect(
testUnicode(count=10),
u"\N{BLACK STAR}" * 10)
@stringfunction
def testmyrow(name, values):
'''
| $name | ${{
for val in values:
out.append(str(val))
}} |
'''
expect(
testmyrow('prices', [1,2,3]),
"| prices | 123 |
\n")
try:
got_exception = ''
def dummy_for_line(): pass
@stringfunction
def testsyntaxerror():
# extra line here
# another extra line here
'''
some text
$a$<'''
except SyntaxError, e:
got_exception = str(e).split(':')[-1]
expect(got_exception, str(dummy_for_line.func_code.co_firstlineno + 7))
try:
got_line = 0
def dummy_for_line2(): pass
@stringfunction
def testruntimeerror(a):
'''
some $a text
${{
out.append(a) # just using up more lines
}}
some more text
$b text $a again'''
expect(testruntimeerror.func_code.co_firstlineno,
dummy_for_line2.func_code.co_firstlineno + 1)
testruntimeerror('hello')
except NameError, e:
import traceback
_, got_line, _, _ = traceback.extract_tb(sys.exc_info()[2], 10)[-1]
expect(got_line, dummy_for_line2.func_code.co_firstlineno + 9)
exec("""if True:
@stringfunction
def testnosource(a):
"${[c for c in reversed(a)]} is '$a' backwards." """
)
expect(testnosource("hello"), "olleh is 'hello' backwards.")
error_line = None
try:
exec("""if True:
@stringfunction
def testnosource_error(a):
"${[c for c in reversed a]} is '$a' backwards." """
)
except SyntaxError, e:
error_line = re.search('line [0-9]*', str(e)).group(0)
expect(error_line, 'line 4')
if ok: print "OK"
else: print "FAIL"