Skip to content

Instantly share code, notes, and snippets.

@pyokagan
Created April 3, 2014 13:32
Show Gist options
  • Select an option

  • Save pyokagan/9954389 to your computer and use it in GitHub Desktop.

Select an option

Save pyokagan/9954389 to your computer and use it in GitHub Desktop.
Simple python templates
r"""Inline python
USAGE
------
`render()` compiles and renders a template, returning a string:
>>> render(r'Hello {{name}}!', name='World')
'Hello World!'
`compile()` compiles a template into a function for later use:
>>> func = compile(r'Hello {{name}}!', argspec=(('name',),))
>>> func('World')
'Hello World!'
SYNTAX
-------
Inline expressions are of the form {{...}}. Any single python expression
that can be evaluated using `eval()` is allowed.
>>> render(r'{{1 + 1}}')
'2'
Control structures are also allowed within inline expressions.
However, they have to be explictly closed using the special keyword `end`.
>>> render(r'{{for x in range(10):}}{{x}} {{end}}')
'0 1 2 3 4 5 6 7 8 9 '
Blocks of python code can be embedded using <%...%>.
>>> render(r'<%x = 42%>x is {{x}}')
'x is 42'
Tokens can be escaped using the backslash character.
>>> render(r'\<%x = 42\%>')
'<%x = 42%>'
"""
# TODO: Error reporting
# TODO: Can probably make code smaller.
from __future__ import print_function
import re
import sys
import argparse
import inspect
__compile = compile
class Parser(object):
# Regular expression shamelessly copied from bottle.py... for now
_re_tok = r"""
# 1: All kinds of python strings
([urbURB]?
(?:''(?!') # Empty string
| ""(?!") # Empty string
| '{6} # Empty string
| "{6} # Empty string
| '(?:[^\\']|\\.)+?'
| "(?:[^\\"]|\\.)+?"
| '{3}(?:[^\\]|\\.|\n)+?'{3}
| "{3}(?:[^\\]|\\.|\n)+?"{3}
)
)
# 2: Comments (until end of line, but not the newline itself)
| (\#.*)
# 3, 4: Keywords that start or continue a python block (only start of line)
| ^([ \t]*(?:if|for|while|with|try|def|class)\b)
| ^([ \t]*(?:elif|else|except|finally)\b)
# 5: Out special 'end' keyword (but only if it stands alone)
| ((?:^|;)[ \t]*end[ \t]*(?=(?:%(inline_end)s[ \t]*)?\r?|;|\#))
# 6: End of code block token
| (%(inline_end)s | %(block_end)s)
# 7: A single newline
| (\r?\n)
"""
# Start of inline or block code
_re_split = r'(\\?)((%(inline_start)s)|(%(block_start)s))'
def __init__(self, block_start='<%', block_end='%>', inline_start='{{',
inline_end='}}', listname='out'):
self.block_start = block_start
self.block_end = block_end
self.inline_start = inline_start
self.inline_end = inline_end
self.listname = listname
pattern_vars = {'block_start': block_start, 'block_end': block_end,
'inline_start': inline_start, 'inline_end': inline_end}
self.re_tok = re.compile(self._re_tok % pattern_vars,
re.MULTILINE | re.VERBOSE)
self.re_split = re.compile(self._re_split % pattern_vars,
re.MULTILINE)
self.indent_cur = 0 # Current indentation level
self.indent_mod = 0 # Indentation level modification after current line
self.out = [] # Output
self.text_buffer = []
self.src = None
self.offset = 0
def write(self, line, strip=False):
if strip:
line = line.lstrip()
if line:
self.out.append(' ' * self.indent_cur + line)
# Apply indent modification
self.indent_cur += self.indent_mod
self.indent_mod = 0
def flush_text(self):
text = ''.join(self.text_buffer)
del self.text_buffer[:]
if text:
self.write('{}.append({!r})'.format(self.listname, text))
def parse(self, src):
self.src = src
self.offset = 0
while True:
m = self.re_split.search(self.src[self.offset:])
if m:
text = self.src[self.offset:self.offset + m.start()]
self.text_buffer.append(text)
self.offset += m.end()
if m.group(1):
# Escape syntax
self.text_buffer.append(m.group(2))
continue
# Start of code
self.flush_text()
self.parse_code(inline=bool(m.group(3)))
else:
break
self.text_buffer.append(self.src[self.offset:])
self.flush_text()
def parse_code(self, inline):
# In block mode everything will be passed as it is to
# write() at the current indentation level
# In inline mode control blocks will modify the current
# indentation level.
line = '' # Line buffer
strip = True if inline else False
is_control = False
def end_block():
if inline:
if is_control:
self.write(line.strip())
elif line.strip():
self.write('{}.append(str(eval({!r})))'.format(self.listname, line.strip()))
else:
self.write(line.rstrip())
while True:
m = self.re_tok.search(self.src[self.offset:])
if not m:
raise Exception('Non-terminated code block')
line += self.src[self.offset:self.offset + m.start()]
self.offset += m.end()
_str, _com, _blk1, _blk2, _end, _cend, _nl = m.groups()
if line and (_blk1 or _blk2): # a if b else c
line += _blk1 or _blk2
continue
if _str: # Python string
line += _str
elif _com: # Python comment (up to EOL)
# Comment can still end with block_end or inline_end
if _com.strip().endswith(self.block_end if block else self.inline_end):
end_block()
return
elif _blk1: # Start of block keyword
line = _blk1
is_control = True
if inline:
self.indent_mod += 1
elif _blk2:
line = _blk2
is_control = True
if inline:
self.indent_cur -= 1
self.indent_mod += 1
elif _end:
line = ''
is_control = True
if inline:
self.indent_mod -= 1
elif _cend:
# End of block
end_block()
return
else: # \n
if not inline:
self.write(line.rstrip(), strip)
line = ''
is_control = False
def stringfunction(f):
globals, locals = sys.modules[f.__module__].__dict__, {}
func_code = getattr(f, '__code__', None) or f.func_code
filename = func_code.co_filename
args = inspect.getargspec(f)
return compile(f.__doc__, name=f.__name__, filename=filename,
argspec=inspect.getargspec(f))
def compile(src, name='template', filename='<string>', argspec=(),
listname='out'):
"""Compiles template `src` into a function"""
locals = {}
p = Parser(listname=listname)
p.parse(src)
out = ['def {}{}:'.format(name, inspect.formatargspec(argspec)),
' {} = []'.format(listname)]
out.extend([' ' + x for x in p.out])
out.append(" return ''.join({})".format(listname))
code = __compile('\n'.join(out), filename, 'exec')
eval(code, globals(), locals)
return locals[name]
def render(src, filename='<string>', listname='out', **kwargs):
p = Parser(listname=listname)
p.parse(src)
out = ['{} = []'.format(listname)]
out.extend(p.out)
code = __compile('\n'.join(out), filename, 'exec')
eval(code, globals(), kwargs)
return ''.join(kwargs[listname])
def main(args):
p = argparse.ArgumentParser()
p.add_argument('-o', '--output', type=argparse.FileType('w'),
default=sys.stdout, metavar='FILE')
p.add_argument('template', nargs='?', default=sys.stdin,
type=argparse.FileType('r'))
args = p.parse_args(args)
args.output.write(render(args.template.read()))
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment