Roll src/third_party/WebKit 06cb9e9:a978ee5 (svn 202558:202559)
[chromium-blink-merge.git] / third_party / pycoverage / coverage / templite.py
blobe5c0bafefb7723d04845b3f7c4a2747c052d577f
1 """A simple Python template renderer, for a nano-subset of Django syntax."""
3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/
5 import re
7 from coverage.backward import set # pylint: disable=W0622
10 class CodeBuilder(object):
11 """Build source code conveniently."""
13 def __init__(self, indent=0):
14 self.code = []
15 self.indent_amount = indent
17 def add_line(self, line):
18 """Add a line of source to the code.
20 Don't include indentations or newlines.
22 """
23 self.code.append(" " * self.indent_amount)
24 self.code.append(line)
25 self.code.append("\n")
27 def add_section(self):
28 """Add a section, a sub-CodeBuilder."""
29 sect = CodeBuilder(self.indent_amount)
30 self.code.append(sect)
31 return sect
33 def indent(self):
34 """Increase the current indent for following lines."""
35 self.indent_amount += 4
37 def dedent(self):
38 """Decrease the current indent for following lines."""
39 self.indent_amount -= 4
41 def __str__(self):
42 return "".join([str(c) for c in self.code])
44 def get_function(self, fn_name):
45 """Compile the code, and return the function `fn_name`."""
46 assert self.indent_amount == 0
47 g = {}
48 code_text = str(self)
49 exec(code_text, g)
50 return g[fn_name]
53 class Templite(object):
54 """A simple template renderer, for a nano-subset of Django syntax.
56 Supported constructs are extended variable access::
58 {{var.modifer.modifier|filter|filter}}
60 loops::
62 {% for var in list %}...{% endfor %}
64 and ifs::
66 {% if var %}...{% endif %}
68 Comments are within curly-hash markers::
70 {# This will be ignored #}
72 Construct a Templite with the template text, then use `render` against a
73 dictionary context to create a finished string.
75 """
76 def __init__(self, text, *contexts):
77 """Construct a Templite with the given `text`.
79 `contexts` are dictionaries of values to use for future renderings.
80 These are good for filters and global values.
82 """
83 self.text = text
84 self.context = {}
85 for context in contexts:
86 self.context.update(context)
88 # We construct a function in source form, then compile it and hold onto
89 # it, and execute it to render the template.
90 code = CodeBuilder()
92 code.add_line("def render(ctx, dot):")
93 code.indent()
94 vars_code = code.add_section()
95 self.all_vars = set()
96 self.loop_vars = set()
97 code.add_line("result = []")
98 code.add_line("a = result.append")
99 code.add_line("e = result.extend")
100 code.add_line("s = str")
102 buffered = []
103 def flush_output():
104 """Force `buffered` to the code builder."""
105 if len(buffered) == 1:
106 code.add_line("a(%s)" % buffered[0])
107 elif len(buffered) > 1:
108 code.add_line("e([%s])" % ",".join(buffered))
109 del buffered[:]
111 # Split the text to form a list of tokens.
112 toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
114 ops_stack = []
115 for tok in toks:
116 if tok.startswith('{{'):
117 # An expression to evaluate.
118 buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip()))
119 elif tok.startswith('{#'):
120 # Comment: ignore it and move on.
121 continue
122 elif tok.startswith('{%'):
123 # Action tag: split into words and parse further.
124 flush_output()
125 words = tok[2:-2].strip().split()
126 if words[0] == 'if':
127 # An if statement: evaluate the expression to determine if.
128 assert len(words) == 2
129 ops_stack.append('if')
130 code.add_line("if %s:" % self.expr_code(words[1]))
131 code.indent()
132 elif words[0] == 'for':
133 # A loop: iterate over expression result.
134 assert len(words) == 4 and words[2] == 'in'
135 ops_stack.append('for')
136 self.loop_vars.add(words[1])
137 code.add_line(
138 "for c_%s in %s:" % (
139 words[1],
140 self.expr_code(words[3])
143 code.indent()
144 elif words[0].startswith('end'):
145 # Endsomething. Pop the ops stack
146 end_what = words[0][3:]
147 if ops_stack[-1] != end_what:
148 raise SyntaxError("Mismatched end tag: %r" % end_what)
149 ops_stack.pop()
150 code.dedent()
151 else:
152 raise SyntaxError("Don't understand tag: %r" % words[0])
153 else:
154 # Literal content. If it isn't empty, output it.
155 if tok:
156 buffered.append("%r" % tok)
157 flush_output()
159 for var_name in self.all_vars - self.loop_vars:
160 vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name))
162 if ops_stack:
163 raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1])
165 code.add_line("return ''.join(result)")
166 code.dedent()
167 self.render_function = code.get_function('render')
169 def expr_code(self, expr):
170 """Generate a Python expression for `expr`."""
171 if "|" in expr:
172 pipes = expr.split("|")
173 code = self.expr_code(pipes[0])
174 for func in pipes[1:]:
175 self.all_vars.add(func)
176 code = "c_%s(%s)" % (func, code)
177 elif "." in expr:
178 dots = expr.split(".")
179 code = self.expr_code(dots[0])
180 args = [repr(d) for d in dots[1:]]
181 code = "dot(%s, %s)" % (code, ", ".join(args))
182 else:
183 self.all_vars.add(expr)
184 code = "c_%s" % expr
185 return code
187 def render(self, context=None):
188 """Render this template by applying it to `context`.
190 `context` is a dictionary of values to use in this rendering.
193 # Make the complete context we'll use.
194 ctx = dict(self.context)
195 if context:
196 ctx.update(context)
197 return self.render_function(ctx, self.do_dots)
199 def do_dots(self, value, *dots):
200 """Evaluate dotted expressions at runtime."""
201 for dot in dots:
202 try:
203 value = getattr(value, dot)
204 except AttributeError:
205 value = value[dot]
206 if hasattr(value, '__call__'):
207 value = value()
208 return value