diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2014-04-26 12:13:48 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2014-04-26 12:13:48 -0400 |
commit | 1a799e2fbc4c1d8eb812b81e5cde39cc16d73538 (patch) | |
tree | 04c899412b52c92c17ca6f8552c8fa11d0f7c991 | |
parent | e832d01fa66613e8037197861bcd2dd8b3cd9bfe (diff) | |
download | python-coveragepy-git-1a799e2fbc4c1d8eb812b81e5cde39cc16d73538.tar.gz |
Polished-up code from 500lines
-rw-r--r-- | coverage/templite.py | 68 | ||||
-rw-r--r-- | tests/test_templite.py | 39 |
2 files changed, 71 insertions, 36 deletions
diff --git a/coverage/templite.py b/coverage/templite.py index 1829aa82..3f6ef0b3 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -8,6 +8,8 @@ import re class CodeBuilder(object): """Build source code conveniently.""" + INDENT_STEP = 4 # PEP8 says so! + def __init__(self, indent=0): self.code = [] self.indent_amount = indent @@ -18,9 +20,7 @@ class CodeBuilder(object): Don't include indentations or newlines. """ - self.code.append(" " * self.indent_amount) - self.code.append(line) - self.code.append("\n") + self.code.extend([" " * self.indent_amount, line, "\n"]) def add_section(self): """Add a section, a sub-CodeBuilder.""" @@ -30,22 +30,25 @@ class CodeBuilder(object): def indent(self): """Increase the current indent for following lines.""" - self.indent_amount += 4 + self.indent_amount += self.INDENT_STEP def dedent(self): """Decrease the current indent for following lines.""" - self.indent_amount -= 4 + self.indent_amount -= self.INDENT_STEP def __str__(self): return "".join(str(c) for c in self.code) - def get_function(self, fn_name): - """Compile the code, and return the function `fn_name`.""" + def get_globals(self): + """Compile the code, and return a dict of globals it defines.""" + # A check that the caller really finished all the blocks they started. assert self.indent_amount == 0 - g = {} - code_text = str(self) - exec(code_text, g) - return g[fn_name] + # Get the Python source as a single string. + python_source = str(self) + # Execute the source, defining globals, and return them. + global_namespace = {} + exec(python_source, global_namespace) + return global_namespace class Templite(object): @@ -83,6 +86,9 @@ class Templite(object): for context in contexts: self.context.update(context) + self.all_vars = set() + self.loop_vars = set() + # We construct a function in source form, then compile it and hold onto # it, and execute it to render the template. code = CodeBuilder() @@ -90,8 +96,6 @@ class Templite(object): code.add_line("def render(ctx, dot):") code.indent() vars_code = code.add_section() - self.all_vars = set() - self.loop_vars = set() code.add_line("result = []") code.add_line("a = result.append") code.add_line("e = result.extend") @@ -107,29 +111,31 @@ class Templite(object): del buffered[:] # Split the text to form a list of tokens. - toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) + tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) ops_stack = [] - for tok in toks: - if tok.startswith('{{'): + for token in tokens: + if token.startswith('{{'): # An expression to evaluate. - buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip())) - elif tok.startswith('{#'): + buffered.append("s(%s)" % self.expr_code(token[2:-2].strip())) + elif token.startswith('{#'): # Comment: ignore it and move on. continue - elif tok.startswith('{%'): + elif token.startswith('{%'): # Action tag: split into words and parse further. flush_output() - words = tok[2:-2].strip().split() + words = token[2:-2].strip().split() if words[0] == 'if': # An if statement: evaluate the expression to determine if. - assert len(words) == 2 + if len(words) != 2: + self.syntax_error("Don't understand if", token) ops_stack.append('if') code.add_line("if %s:" % self.expr_code(words[1])) code.indent() elif words[0] == 'for': # A loop: iterate over expression result. - assert len(words) == 4 and words[2] == 'in' + if len(words) != 4 or words[2] != 'in': + self.syntax_error("Don't understand for", token) ops_stack.append('for') self.loop_vars.add(words[1]) code.add_line( @@ -140,29 +146,33 @@ class Templite(object): ) code.indent() elif words[0].startswith('end'): - # Endsomething. Pop the ops stack + # Endsomething. Pop the ops stack. end_what = words[0][3:] if ops_stack[-1] != end_what: - raise SyntaxError("Mismatched end tag: %r" % end_what) + self.syntax_error("Mismatched end tag", end_what) ops_stack.pop() code.dedent() else: - raise SyntaxError("Don't understand tag: %r" % words[0]) + self.syntax_error("Don't understand tag", words[0]) else: # Literal content. If it isn't empty, output it. - if tok: - buffered.append("%r" % tok) + if token: + buffered.append("%r" % token) flush_output() for var_name in self.all_vars - self.loop_vars: vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name)) if ops_stack: - raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1]) + self.syntax_error("Unmatched action tag", ops_stack[-1]) code.add_line("return ''.join(result)") code.dedent() - self.render_function = code.get_function('render') + self.render_function = code.get_globals()['render'] + + def syntax_error(self, msg, thing): + """Raise a syntax error using `msg`, and showing `thing`.""" + raise SyntaxError("%s: %r" % (msg, thing)) def expr_code(self, expr): """Generate a Python expression for `expr`.""" diff --git a/tests/test_templite.py b/tests/test_templite.py index 48e53ab4..c861c2ff 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -23,9 +23,15 @@ class TempliteTest(CoverageTest): run_in_temp_dir = False - def try_render(self, text, ctx, result): - """Render `text` through `ctx`, and it had better be `result`.""" - self.assertEqual(Templite(text).render(ctx), result) + def try_render(self, text, ctx=None, result=None): + """Render `text` through `ctx`, and it had better be `result`. + + Result defaults to None so we can shorten the calls where we expect + an exception and never get to the result comparison. + """ + actual = Templite(text).render(ctx or {}) + if result: + self.assertEqual(actual, result) def test_passthrough(self): # Strings without variables are passed through unchanged. @@ -42,7 +48,7 @@ class TempliteTest(CoverageTest): def test_undefined_variables(self): # Using undefined names is an error. with self.assertRaises(Exception): - self.try_render("Hi, {{name}}!", {}, "xyz") + self.try_render("Hi, {{name}}!") def test_pipes(self): # Variables can be filtered with pipes. @@ -228,12 +234,31 @@ class TempliteTest(CoverageTest): def test_bogus_tag_syntax(self): msg = "Don't understand tag: 'bogus'" with self.assertRaisesRegexp(SyntaxError, msg): - self.try_render("Huh: {% bogus %}!!{% endbogus %}??", {}, "") + self.try_render("Huh: {% bogus %}!!{% endbogus %}??") + + def test_malformed_if(self): + msg = "Don't understand if: '{% if %}'" + with self.assertRaisesRegexp(SyntaxError, msg): + self.try_render("Buh? {% if %}hi!{% endif %}") + msg = "Don't understand if: '{% if this or that %}'" + with self.assertRaisesRegexp(SyntaxError, msg): + self.try_render("Buh? {% if this or that %}hi!{% endif %}") + + def test_malformed_for_(self): + msg = "Don't understand for: '{% for %}'" + with self.assertRaisesRegexp(SyntaxError, msg): + self.try_render("Weird: {% for %}loop{% endfor %}") + msg = "Don't understand for: '{% for x from y %}'" + with self.assertRaisesRegexp(SyntaxError, msg): + self.try_render("Weird: {% for x from y %}loop{% endfor %}") + msg = "Don't understand for: '{% for x, y in z %}'" + with self.assertRaisesRegexp(SyntaxError, msg): + self.try_render("Weird: {% for x, y in z %}loop{% endfor %}") def test_bad_nesting(self): msg = "Unmatched action tag: 'if'" with self.assertRaisesRegexp(SyntaxError, msg): - self.try_render("{% if x %}X", {}, "") + self.try_render("{% if x %}X") msg = "Mismatched end tag: 'for'" with self.assertRaisesRegexp(SyntaxError, msg): - self.try_render("{% if x %}X{% endfor %}", {}, "") + self.try_render("{% if x %}X{% endfor %}") |