diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2014-05-09 07:00:49 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2014-05-09 07:00:49 -0400 |
commit | dcfafdac94a31a755b958d6f47ffd09e38ea9833 (patch) | |
tree | 2283828c87b9c8f7c25a100ffb0177c5200a98b7 | |
parent | f13c8f4d2b214f6099e927faf7e79584283f77c8 (diff) | |
download | python-coveragepy-git-dcfafdac94a31a755b958d6f47ffd09e38ea9833.tar.gz |
Latest code form 500lines
-rw-r--r-- | coverage/templite.py | 135 | ||||
-rw-r--r-- | tests/test_templite.py | 49 |
2 files changed, 116 insertions, 68 deletions
diff --git a/coverage/templite.py b/coverage/templite.py index 3f6ef0b3..a71caf63 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -5,44 +5,49 @@ import re +class TempliteSyntaxError(ValueError): + """Raised when a template has a syntax error.""" + pass + + class CodeBuilder(object): """Build source code conveniently.""" - INDENT_STEP = 4 # PEP8 says so! - def __init__(self, indent=0): self.code = [] - self.indent_amount = indent + self.ident_level = indent + + def __str__(self): + return "".join(str(c) for c in self.code) def add_line(self, line): """Add a line of source to the code. - Don't include indentations or newlines. + Indentation and newline will be added for you, don't provide them. """ - self.code.extend([" " * self.indent_amount, line, "\n"]) + self.code.extend([" " * self.ident_level, line, "\n"]) - def add_section(self): + def add_subbuilder(self): """Add a section, a sub-CodeBuilder.""" - sect = CodeBuilder(self.indent_amount) + sect = CodeBuilder(self.ident_level) self.code.append(sect) return sect + INDENT_STEP = 4 # PEP8 says so! + def indent(self): """Increase the current indent for following lines.""" - self.indent_amount += self.INDENT_STEP + self.ident_level += self.INDENT_STEP def dedent(self): """Decrease the current indent for following lines.""" - self.indent_amount -= self.INDENT_STEP - - def __str__(self): - return "".join(str(c) for c in self.code) + self.ident_level -= self.INDENT_STEP 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 + assert self.ident_level == 0 # Get the Python source as a single string. python_source = str(self) # Execute the source, defining globals, and return them. @@ -71,7 +76,20 @@ class Templite(object): {# This will be ignored #} Construct a Templite with the template text, then use `render` against a - dictionary context to create a finished string. + dictionary context to create a finished string:: + + templite = Templite(''' + <h1>Hello {{name|upper}}!</h1> + {% for topic in topics %} + <p>You are interested in {{topic}}.</p> + {% endif %} + ''', + {'upper': str.upper}, + ) + text = templite.render({ + 'name': "Ned", + 'topics': ['Python', 'Geometry', 'Juggling'], + }) """ def __init__(self, text, *contexts): @@ -81,7 +99,6 @@ class Templite(object): These are good for filters and global values. """ - self.text = text self.context = {} for context in contexts: self.context.update(context) @@ -93,9 +110,9 @@ class Templite(object): # it, and execute it to render the template. code = CodeBuilder() - code.add_line("def render(ctx, dot):") + code.add_line("def render_function(ctx, do_dots):") code.indent() - vars_code = code.add_section() + vars_code = code.add_subbuilder() code.add_line("result = []") code.add_line("a = result.append") code.add_line("e = result.extend") @@ -107,20 +124,21 @@ class Templite(object): if len(buffered) == 1: code.add_line("a(%s)" % buffered[0]) elif len(buffered) > 1: - code.add_line("e([%s])" % ",".join(buffered)) + code.add_line("e([%s])" % ", ".join(buffered)) del buffered[:] + ops_stack = [] + # Split the text to form a list of tokens. tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) - ops_stack = [] for token in tokens: - if token.startswith('{{'): - # An expression to evaluate. - buffered.append("s(%s)" % self.expr_code(token[2:-2].strip())) - elif token.startswith('{#'): + if token.startswith('{#'): # Comment: ignore it and move on. continue + elif token.startswith('{{'): + # An expression to evaluate. + buffered.append("s(%s)" % self._expr_code(token[2:-2].strip())) elif token.startswith('{%'): # Action tag: split into words and parse further. flush_output() @@ -128,70 +146,87 @@ class Templite(object): if words[0] == 'if': # An if statement: evaluate the expression to determine if. if len(words) != 2: - self.syntax_error("Don't understand if", token) + self._syntax_error("Don't understand if", token) ops_stack.append('if') - code.add_line("if %s:" % self.expr_code(words[1])) + code.add_line("if %s:" % self._expr_code(words[1])) code.indent() elif words[0] == 'for': # A loop: iterate over expression result. if len(words) != 4 or words[2] != 'in': - self.syntax_error("Don't understand for", token) + self._syntax_error("Don't understand for", token) ops_stack.append('for') - self.loop_vars.add(words[1]) + self._variable(words[1], self.loop_vars) code.add_line( "for c_%s in %s:" % ( words[1], - self.expr_code(words[3]) + self._expr_code(words[3]) ) ) code.indent() elif words[0].startswith('end'): # Endsomething. Pop the ops stack. + if len(words) != 1: + self._syntax_error("Don't understand end", token) end_what = words[0][3:] - if ops_stack[-1] != end_what: - self.syntax_error("Mismatched end tag", end_what) - ops_stack.pop() + if not ops_stack: + self._syntax_error("Too many ends", token) + start_what = ops_stack.pop() + if start_what != end_what: + self._syntax_error("Mismatched end tag", end_what) code.dedent() else: - self.syntax_error("Don't understand tag", words[0]) + self._syntax_error("Don't understand tag", words[0]) else: # Literal content. If it isn't empty, output it. if token: - buffered.append("%r" % token) + buffered.append(repr(token)) + + if ops_stack: + self._syntax_error("Unmatched action tag", ops_stack[-1]) + 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: - self.syntax_error("Unmatched action tag", ops_stack[-1]) - code.add_line("return ''.join(result)") code.dedent() - 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)) + self._render_function = code.get_globals()['render_function'] - def expr_code(self, expr): + def _expr_code(self, expr): """Generate a Python expression for `expr`.""" if "|" in expr: pipes = expr.split("|") - code = self.expr_code(pipes[0]) + code = self._expr_code(pipes[0]) for func in pipes[1:]: - self.all_vars.add(func) + self._variable(func, self.all_vars) code = "c_%s(%s)" % (func, code) elif "." in expr: dots = expr.split(".") - code = self.expr_code(dots[0]) + code = self._expr_code(dots[0]) args = ", ".join(repr(d) for d in dots[1:]) - code = "dot(%s, %s)" % (code, args) + code = "do_dots(%s, %s)" % (code, args) else: - self.all_vars.add(expr) + self._variable(expr, self.all_vars) code = "c_%s" % expr return code + def _syntax_error(self, msg, thing): + """Raise a syntax error using `msg`, and showing `thing`.""" + raise TempliteSyntaxError("%s: %r" % (msg, thing)) + + def _variable(self, name, vars_set): + """Track that `name` is used as a variable. + + Adds the name to `vars_set`, a set of variable names. + + Raises an syntax error if `name` is not a valid name. + + """ + if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name): + self._syntax_error("Not a valid name", name) + vars_set.add(name) + def render(self, context=None): """Render this template by applying it to `context`. @@ -202,15 +237,15 @@ class Templite(object): ctx = dict(self.context) if context: ctx.update(context) - return self.render_function(ctx, self.do_dots) + return self._render_function(ctx, self._do_dots) - def do_dots(self, value, *dots): + def _do_dots(self, value, *dots): """Evaluate dotted expressions at runtime.""" for dot in dots: try: value = getattr(value, dot) except AttributeError: value = value[dot] - if hasattr(value, '__call__'): + if callable(value): value = value() return value diff --git a/tests/test_templite.py b/tests/test_templite.py index c861c2ff..4b1f6e45 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -1,6 +1,7 @@ """Tests for coverage.templite.""" -from coverage.templite import Templite +import re +from coverage.templite import Templite, TempliteSyntaxError from tests.coveragetest import CoverageTest # pylint: disable=W0612,E1101 @@ -33,6 +34,10 @@ class TempliteTest(CoverageTest): if result: self.assertEqual(actual, result) + def assertSynErr(self, msg): + pat = "^" + re.escape(msg) + "$" + return self.assertRaisesRegexp(TempliteSyntaxError, pat) + def test_passthrough(self): # Strings without variables are passed through unchanged. self.assertEqual(Templite("Hello").render(), "Hello") @@ -231,34 +236,42 @@ class TempliteTest(CoverageTest): "Hey {{foo.bar.baz}} there", {'foo': None}, "Hey ??? there" ) + def test_bad_names(self): + with self.assertSynErr("Not a valid name: 'var%&!@'"): + self.try_render("Wat: {{ var%&!@ }}") + with self.assertSynErr("Not a valid name: 'filter%&!@'"): + self.try_render("Wat: {{ foo|filter%&!@ }}") + with self.assertSynErr("Not a valid name: '@'"): + self.try_render("Wat: {% for @ in x %}{% endfor %}") + def test_bogus_tag_syntax(self): - msg = "Don't understand tag: 'bogus'" - with self.assertRaisesRegexp(SyntaxError, msg): + with self.assertSynErr("Don't understand tag: 'bogus'"): self.try_render("Huh: {% bogus %}!!{% endbogus %}??") def test_malformed_if(self): - msg = "Don't understand if: '{% if %}'" - with self.assertRaisesRegexp(SyntaxError, msg): + with self.assertSynErr("Don't understand if: '{% if %}'"): self.try_render("Buh? {% if %}hi!{% endif %}") - msg = "Don't understand if: '{% if this or that %}'" - with self.assertRaisesRegexp(SyntaxError, msg): + with self.assertSynErr("Don't understand if: '{% if this or that %}'"): 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): + def test_malformed_for(self): + with self.assertSynErr("Don't understand for: '{% for %}'"): self.try_render("Weird: {% for %}loop{% endfor %}") - msg = "Don't understand for: '{% for x from y %}'" - with self.assertRaisesRegexp(SyntaxError, msg): + with self.assertSynErr("Don't understand for: '{% for x from y %}'"): 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): + with self.assertSynErr("Don't understand for: '{% for x, y in z %}'"): 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): + with self.assertSynErr("Unmatched action tag: 'if'"): self.try_render("{% if x %}X") - msg = "Mismatched end tag: 'for'" - with self.assertRaisesRegexp(SyntaxError, msg): + with self.assertSynErr("Mismatched end tag: 'for'"): self.try_render("{% if x %}X{% endfor %}") + with self.assertSynErr("Too many ends: '{% endif %}'"): + self.try_render("{% if x %}{% endif %}{% endif %}") + + def test_malformed_end(self): + with self.assertSynErr("Don't understand end: '{% end if %}'"): + self.try_render("{% if x %}X{% end if %}") + with self.assertSynErr("Don't understand end: '{% endif now %}'"): + self.try_render("{% if x %}X{% endif now %}") |