summaryrefslogtreecommitdiff
path: root/coverage/templite.py
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2014-05-09 07:00:49 -0400
committerNed Batchelder <ned@nedbatchelder.com>2014-05-09 07:00:49 -0400
commit2616772b7b33e76487ab9f16bcabb7c5aeb5feaf (patch)
treed91bf34da9268596928b316b307b21fc5b835b22 /coverage/templite.py
parent670090a49cb1a923198f465a2aa455ba18cda3dc (diff)
downloadpython-coveragepy-2616772b7b33e76487ab9f16bcabb7c5aeb5feaf.tar.gz
Latest code form 500lines
Diffstat (limited to 'coverage/templite.py')
-rw-r--r--coverage/templite.py135
1 files changed, 85 insertions, 50 deletions
diff --git a/coverage/templite.py b/coverage/templite.py
index 3f6ef0b..a71caf6 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