Created
April 3, 2014 13:32
-
-
Save pyokagan/9954389 to your computer and use it in GitHub Desktop.
Simple python templates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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