| # -*- coding: utf-8 -*- |
| """ |
| jinja.translators.python |
| ~~~~~~~~~~~~~~~~~~~~~~~~ |
| |
| This module translates a jinja ast into python code. |
| |
| This translator tries hard to keep Jinja sandboxed. All security |
| relevant calls are wrapped by methods defined in the environment. |
| This affects: |
| |
| - method calls |
| - attribute access |
| - name resolution |
| |
| It also adds debug symbols used by the traceback toolkit implemented |
| in `jinja.utils`. |
| |
| Implementation Details |
| ====================== |
| |
| It might sound strange but the translator tries to keep the generated |
| code readable as much as possible. This simplifies debugging the Jinja |
| core a lot. The additional processing overhead is just relevant for |
| the translation process, the additional comments and whitespace won't |
| appear in the saved bytecode. |
| |
| :copyright: 2007 by Armin Ronacher. |
| :license: BSD, see LICENSE for more details. |
| """ |
| import re |
| import sys |
| from jinja import nodes |
| from jinja.nodes import get_nodes |
| from jinja.parser import Parser |
| from jinja.exceptions import TemplateSyntaxError |
| from jinja.translators import Translator |
| from jinja.datastructure import TemplateStream |
| from jinja.utils import set, capture_generator |
| |
| |
| #: regular expression for the debug symbols |
| _debug_re = re.compile(r'^\s*\# DEBUG\(filename=(?P<filename>.*?), ' |
| r'lineno=(?P<lineno>\d+)\)$') |
| |
| # For Python versions without generator exit exceptions |
| try: |
| GeneratorExit = GeneratorExit |
| except NameError: |
| class GeneratorExit(Exception): |
| pass |
| |
| # For Pythons without conditional expressions |
| try: |
| exec '0 if 0 else 0' |
| have_conditional_expr = True |
| except SyntaxError: |
| have_conditional_expr = False |
| |
| |
| class Template(object): |
| """ |
| Represents a finished template. |
| """ |
| |
| def __init__(self, environment, code): |
| self.environment = environment |
| self.code = code |
| self.generate_func = None |
| |
| def dump(self, stream=None): |
| """Dump the template into python bytecode.""" |
| if stream is not None: |
| from marshal import dump |
| dump(self.code, stream) |
| else: |
| from marshal import dumps |
| return dumps(self.code) |
| |
| def load(environment, data): |
| """Load the template from python bytecode.""" |
| if isinstance(data, basestring): |
| from marshal import loads |
| code = loads(data) |
| else: |
| from marshal import load |
| code = load(data) |
| return Template(environment, code) |
| load = staticmethod(load) |
| |
| def render(self, *args, **kwargs): |
| """Render a template.""" |
| __traceback_hide__ = True |
| ctx = self._prepare(*args, **kwargs) |
| try: |
| return capture_generator(self.generate_func(ctx)) |
| except: |
| self._debug(ctx, *sys.exc_info()) |
| |
| def stream(self, *args, **kwargs): |
| """Render a template as stream.""" |
| def proxy(ctx): |
| try: |
| for item in self.generate_func(ctx): |
| yield item |
| except GeneratorExit: |
| return |
| except: |
| self._debug(ctx, *sys.exc_info()) |
| return TemplateStream(proxy(self._prepare(*args, **kwargs))) |
| |
| def _prepare(self, *args, **kwargs): |
| """Prepare the template execution.""" |
| # if there is no generation function we execute the code |
| # in a new namespace and save the generation function and |
| # debug information. |
| env = self.environment |
| if self.generate_func is None: |
| ns = {'environment': env} |
| exec self.code in ns |
| self.generate_func = ns['generate'] |
| return env.context_class(env, *args, **kwargs) |
| |
| def _debug(self, ctx, exc_type, exc_value, traceback): |
| """Debugging Helper""" |
| # just modify traceback if we have that feature enabled |
| from traceback import print_exception |
| print_exception(exc_type, exc_value, traceback) |
| |
| if self.environment.friendly_traceback: |
| # hook the debugger in |
| from jinja.debugger import translate_exception |
| exc_type, exc_value, traceback = translate_exception( |
| self, ctx, exc_type, exc_value, traceback) |
| print_exception(exc_type, exc_value, traceback) |
| |
| raise exc_type, exc_value, traceback |
| |
| |
| class TranslationOperator(object): |
| """ |
| A translation operator has a single string representing the operation |
| """ |
| def __init__(self, operator, translator): |
| self.operator = operator |
| self.translator = translator |
| |
| class UnaryOperator(TranslationOperator): |
| """ |
| A unary operator has one side to the operation. |
| """ |
| def __call__(self, node): |
| """ |
| Apply operator. |
| """ |
| return '( %s %s)' % ( |
| self.operator, |
| self.translator.handle_node(node.node), |
| ) |
| |
| class BinaryOperator(TranslationOperator): |
| """ |
| A binary operator has two sides to the operation. |
| """ |
| def __call__(self, node): |
| """ |
| Apply operator. |
| """ |
| return '(%s %s %s)' % ( |
| self.translator.handle_node(node.left), |
| self.operator, |
| self.translator.handle_node(node.right), |
| ) |
| |
| class PythonTranslator(Translator): |
| """ |
| Pass this translator a ast tree to get valid python code. |
| """ |
| |
| def __init__(self, environment, node, source): |
| self.environment = environment |
| self.loader = environment.loader.get_controlled_loader() |
| self.node = node |
| self.source = source |
| self.closed = False |
| |
| #: current level of indention |
| self.indention = 0 |
| #: each {% cycle %} tag has a unique ID which increments |
| #: automatically for each tag. |
| self.last_cycle_id = 0 |
| #: set of used shortcuts jinja has to make local automatically |
| self.used_shortcuts = set(['undefined_singleton']) |
| #: set of used datastructures jinja has to import |
| self.used_data_structures = set() |
| #: set of used utils jinja has to import |
| self.used_utils = set() |
| #: flags for runtime error |
| self.require_runtime_error = False |
| #: do wee need a "set" object? |
| self.need_set_import = False |
| #: flag for regular expressions |
| self.compiled_regular_expressions = {} |
| |
| #: bind the nodes to the callback functions. There are |
| #: some missing! A few are specified in the `unhandled` |
| #: mapping in order to disallow their usage, some of them |
| #: will not appear in the jinja parser output because |
| #: they are filtered out. |
| self.handlers = { |
| # block nodes |
| nodes.Template: self.handle_template, |
| nodes.Text: self.handle_template_text, |
| nodes.NodeList: self.handle_node_list, |
| nodes.ForLoop: self.handle_for_loop, |
| nodes.IfCondition: self.handle_if_condition, |
| nodes.Cycle: self.handle_cycle, |
| nodes.Print: self.handle_print, |
| nodes.Macro: self.handle_macro, |
| nodes.Call: self.handle_call, |
| nodes.Set: self.handle_set, |
| nodes.Filter: self.handle_filter, |
| nodes.Block: self.handle_block, |
| nodes.Include: self.handle_include, |
| nodes.Trans: self.handle_trans, |
| |
| # expression nodes |
| nodes.NameExpression: self.handle_name, |
| nodes.CompareExpression: self.handle_compare, |
| nodes.TestExpression: self.handle_test, |
| nodes.ConstantExpression: self.handle_const, |
| nodes.RegexExpression: self.handle_regex, |
| nodes.SubscriptExpression: self.handle_subscript, |
| nodes.FilterExpression: self.handle_filter_expr, |
| nodes.CallExpression: self.handle_call_expr, |
| nodes.AddExpression: BinaryOperator('+', self), |
| nodes.SubExpression: BinaryOperator('-', self), |
| nodes.ConcatExpression: self.handle_concat, |
| nodes.DivExpression: BinaryOperator('/', self), |
| nodes.FloorDivExpression: BinaryOperator('//', self), |
| nodes.MulExpression: BinaryOperator('*', self), |
| nodes.ModExpression: BinaryOperator('%', self), |
| nodes.PosExpression: UnaryOperator('+', self), |
| nodes.NegExpression: UnaryOperator('-', self), |
| nodes.PowExpression: BinaryOperator('**', self), |
| nodes.DictExpression: self.handle_dict, |
| nodes.SetExpression: self.handle_set_expr, |
| nodes.ListExpression: self.handle_list, |
| nodes.TupleExpression: self.handle_tuple, |
| nodes.UndefinedExpression: self.handle_undefined, |
| nodes.AndExpression: BinaryOperator(' and ', self), |
| nodes.OrExpression: BinaryOperator(' or ', self), |
| nodes.NotExpression: UnaryOperator(' not ', self), |
| nodes.SliceExpression: self.handle_slice, |
| nodes.ConditionalExpression: self.handle_conditional_expr |
| } |
| |
| # -- public methods |
| |
| def process(environment, node, source=None): |
| """ |
| The only public method. Creates a translator instance, |
| translates the code and returns it in form of an |
| `Template` instance. |
| """ |
| translator = PythonTranslator(environment, node, source) |
| filename = node.filename or '<template>' |
| source = translator.translate() |
| return Template(environment, compile(source, filename, 'exec')) |
| process = staticmethod(process) |
| |
| # -- private helper methods |
| |
| def indent(self, text): |
| """ |
| Indent the current text. This does only indent the |
| first line. |
| """ |
| return (' ' * (self.indention * 4)) + text |
| |
| def to_tuple(self, args): |
| """ |
| Return a tuple repr without nested repr. |
| """ |
| return '(%s%s)' % ( |
| ', '.join(args), |
| len(args) == 1 and ',' or '' |
| ) |
| |
| def nodeinfo(self, node, force=False): |
| """ |
| Return a comment that helds the node informations or None |
| if there is no need to add a debug comment. |
| """ |
| return '# DEBUG(filename=%s, lineno=%s)' % ( |
| node.filename or '', |
| node.lineno |
| ) |
| |
| def handle_node(self, node): |
| """ |
| Handle one node. Resolves the correct callback functions defined |
| in the callback mapping. |
| """ |
| if self.closed: |
| raise RuntimeError('translator is closed') |
| if node.__class__ in self.handlers: |
| return self.handlers[node.__class__](node) |
| else: |
| raise AssertionError('unhandled node %r' % node.__class__) |
| |
| def close(self): |
| """ |
| Clean up stuff. |
| """ |
| self.closed = True |
| self.handlers = self.node = self.environment = self.loader = None |
| |
| def translate(self): |
| """ |
| Translate the node defined in the constructor. |
| """ |
| try: |
| return self.handle_node(self.node) |
| finally: |
| self.close() |
| |
| # -- jinja nodes |
| |
| def handle_template(self, node): |
| """ |
| Handle the overall template node. This node is the first node and |
| ensures that we get the bootstrapping code. It also knows about |
| inheritance information. It only occours as outer node, never in |
| the tree itself. |
| """ |
| self.indention = 1 |
| |
| # if there is a parent template we parse the parent template and |
| # update the blocks there. Once this is done we drop the current |
| # template in favor of the new one. Do that until we found the |
| # root template. |
| parent = None |
| overwrites = {} |
| blocks = {} |
| requirements = [] |
| outer_filename = node.filename or '<template>' |
| |
| # this set is required in order to not add blocks to the block |
| # dict a second time if they were not overridden in one template |
| # in the template chain. |
| already_registered_block = set() |
| |
| while node.extends is not None: |
| # the direct child nodes in a template that are not blocks |
| # are processed as template globals, thus executed *before* |
| # the master layout template is loaded. This can be used |
| # for further processing. The output of those nodes does |
| # not appear in the final template. |
| requirements += [child for child in node.body.get_child_nodes() |
| if child.__class__ not in (nodes.Text, |
| nodes.Block)] |
| |
| # load the template we inherit from and add not known blocks. |
| # this also marks the templates on the controlled loader but |
| # are never removed. that's no problem because we don't allow |
| # parents we extend from as includes and the controlled loader |
| # is only used for this templated |
| parent = self.loader.parse(node.extends, |
| node.filename) |
| |
| # look up all block nodes in the current template and |
| # add them to the override dict. |
| for n in get_nodes(nodes.Block, node): |
| overwrites[n.name] = n |
| # handle direct overrides |
| for n in get_nodes(nodes.Block, parent): |
| # an overwritten block for the parent template. handle that |
| # override in the template and register it in the deferred |
| # block dict. |
| if n.name in overwrites and n not in already_registered_block: |
| blocks.setdefault(n.name, []).append(n.clone()) |
| n.replace(overwrites[n.name]) |
| already_registered_block.add(n) |
| # make the parent node the new node |
| node = parent |
| |
| # handle requirements code |
| if requirements: |
| requirement_lines = ['def bootstrap(context):'] |
| for n in requirements: |
| requirement_lines.append(self.handle_node(n)) |
| requirement_lines.append(' if 0: yield None\n') |
| |
| # handle body in order to get the used shortcuts |
| body_code = self.handle_node(node.body) |
| |
| # same for blocks in callables |
| block_lines = [] |
| block_items = blocks.items() |
| block_items.sort() |
| dict_lines = [] |
| for name, items in block_items: |
| tmp = [] |
| for idx, item in enumerate(items): |
| # ensure that the indention is correct |
| self.indention = 1 |
| func_name = 'block_%s_%s' % (name, idx) |
| data = self.handle_block(item, idx + 1) |
| # blocks with data |
| if data: |
| block_lines.extend([ |
| 'def %s(context):' % func_name, |
| self.indent(self.nodeinfo(item, True)), |
| data, |
| ' if 0: yield None\n' |
| ]) |
| tmp.append('buffereater(%s)' % func_name) |
| self.used_utils.add('buffereater') |
| # blocks without data, can default to something |
| # from utils |
| else: |
| tmp.append('empty_block') |
| self.used_utils.add('empty_block') |
| dict_lines.append(' %r: %s' % ( |
| str(name), |
| self.to_tuple(tmp) |
| )) |
| |
| # bootstrapping code |
| lines = ['# Essential imports', 'from __future__ import division'] |
| if self.used_utils: |
| lines.append('from jinja.utils import %s' % \ |
| ', '.join(tuple(self.used_utils))) |
| if self.require_runtime_error: |
| lines.append('from jinja.exceptions import TemplateRuntimeError') |
| if self.used_data_structures: |
| lines.append('from jinja.datastructure import %s' % ', '. |
| join(self.used_data_structures)) |
| if self.need_set_import: |
| lines.append('from jinja.utils import set') |
| |
| # compile regular expressions |
| if self.compiled_regular_expressions: |
| lines.append('import re') |
| lines.append('\n# Compile used regular expressions') |
| for regex, name in self.compiled_regular_expressions.iteritems(): |
| lines.append('%s = re.compile(%r)' % (name, regex)) |
| |
| lines.append( |
| '\n# Aliases for some speedup\n' |
| '%s\n\n' |
| '# Marker for Jinja templates\n' |
| '__jinja_template__ = True\n\n' |
| '# Name for disabled debugging\n' |
| '__name__ = %r\n\n' |
| 'def generate(context):\n' |
| ' assert environment is context.environment' % ( |
| '\n'.join([ |
| '%s = environment.%s' % (item, item) for item in |
| self.used_shortcuts |
| ]), |
| outer_filename |
| ) |
| ) |
| |
| # the template body |
| if requirements: |
| lines.append(' for item in bootstrap(context): pass') |
| lines.append(body_code) |
| lines.append(' if 0: yield None\n') |
| |
| # now write the bootstrapping (requirements) core if there is one |
| if requirements: |
| lines.append('# Bootstrapping code') |
| lines.extend(requirement_lines) |
| |
| # blocks must always be defined. even if it's empty. some |
| # features depend on it |
| if block_lines: |
| lines.append('# Superable blocks') |
| lines.extend(block_lines) |
| lines.append('# Block mapping') |
| if dict_lines: |
| lines.append('blocks = {\n%s\n}\n' % ',\n'.join(dict_lines)) |
| else: |
| lines.append('blocks = {}\n') |
| |
| # now get the real source lines and map the debugging symbols |
| debug_mapping = [] |
| file_mapping = {} |
| last = None |
| offset = -1 |
| sourcelines = ('\n'.join(lines)).splitlines() |
| result = [] |
| |
| for idx, line in enumerate(sourcelines): |
| m = _debug_re.search(line) |
| if m is not None: |
| d = m.groupdict() |
| filename = d['filename'] or None |
| if isinstance(filename, unicode): |
| filename = filename.encode('utf-8') |
| if filename in file_mapping: |
| file_id = file_mapping[filename] |
| else: |
| file_id = file_mapping[filename] = 'F%d' % \ |
| len(file_mapping) |
| this = (file_id, int(d['lineno'])) |
| # if it's the same as the line before we ignore it |
| if this != last: |
| debug_mapping.append('(%r, %s, %r)' % ((idx - offset,) + this)) |
| last = this |
| # for each debug symbol the line number and so the offset |
| # changes by one. |
| offset += 1 |
| else: |
| result.append(line) |
| |
| # now print file mapping and debug info |
| # the debug info: |
| # debug_info binds template line numbers to generated |
| # source lines. this information is always |
| # present and part of the bytecode. |
| # template_source only available if loaded from string to |
| # get debug source code. Because this is |
| # dumped too it's a bad idea to dump templates |
| # loaded from a string. |
| result.append('\n# Debug Information') |
| file_mapping = file_mapping.items() |
| file_mapping.sort(lambda a, b: cmp(a[1], b[1])) |
| for filename, file_id in file_mapping: |
| result.append('%s = %r' % (file_id, filename)) |
| result.append('debug_info = %s' % self.to_tuple(debug_mapping)) |
| result.append('template_source = %r' % self.source) |
| |
| return '\n'.join(result) |
| |
| def handle_template_text(self, node): |
| """ |
| Handle data around nodes. |
| """ |
| # special case: no variables |
| if not node.variables: |
| return self.indent(self.nodeinfo(node)) + '\n' + \ |
| self.indent('yield %r' % node.text.replace('%%', '%')) |
| |
| # special case: one variable, no text |
| self.used_shortcuts.add('finish_var') |
| if len(node.variables) == 1 and node.text == '%s': |
| return self.indent(self.nodeinfo(node)) + '\n' + \ |
| self.indent('yield finish_var(%s, context)' % |
| self.handle_node(node.variables[0])) |
| |
| # all other cases |
| buf = [] |
| write = lambda x: buf.append(self.indent(x)) |
| |
| write(self.nodeinfo(node)) |
| write('yield %r %% (' % node.text) |
| self.indention += 1 |
| for var in node.variables: |
| write(self.nodeinfo(var)) |
| write('finish_var(%s, context)' % self.handle_node(var) + ',') |
| self.indention -= 1 |
| write(')') |
| |
| return '\n'.join(buf) |
| |
| def handle_node_list(self, node): |
| """ |
| In some situations we might have a node list. It's just |
| a collection of multiple statements. |
| |
| If the nodelist was empty it will return an empty string |
| """ |
| body = '\n'.join([self.handle_node(n) for n in node]) |
| if body: |
| return self.indent(self.nodeinfo(node)) + '\n' + body |
| return '' |
| |
| def handle_for_loop(self, node): |
| """ |
| Handle a for loop. Pretty basic, just that we give the else |
| clause a different behavior. |
| """ |
| self.used_data_structures.add('LoopContext') |
| buf = [] |
| write = lambda x: buf.append(self.indent(x)) |
| write(self.nodeinfo(node)) |
| write('context.push()') |
| |
| # recursive loops |
| if node.recursive: |
| write('def loop(seq):') |
| self.indention += 1 |
| write('for %s in context[\'loop\'].push(seq):' % |
| self.handle_node(node.item), |
| ) |
| |
| # simple loops |
| else: |
| write('context[\'loop\'] = loop = LoopContext(%s, ' |
| 'context[\'loop\'], None)' % self.handle_node(node.seq)) |
| write('for %s in loop:' % |
| self.handle_node(node.item) |
| ) |
| |
| # handle real loop code |
| self.indention += 1 |
| write(self.nodeinfo(node.body)) |
| if node.body: |
| buf.append(self.handle_node(node.body)) |
| else: |
| write('pass') |
| self.indention -= 1 |
| |
| # else part of loop |
| if node.else_: |
| write('if not context[\'loop\'].iterated:') |
| self.indention += 1 |
| write(self.nodeinfo(node.else_)) |
| buf.append(self.handle_node(node.else_) or self.indent('pass')) |
| self.indention -= 1 |
| |
| # call recursive for loop! |
| if node.recursive: |
| write('context[\'loop\'].pop()') |
| write('if 0: yield None') |
| self.indention -= 1 |
| write('context[\'loop\'] = LoopContext(None, context[\'loop\'], ' |
| 'buffereater(loop))') |
| self.used_utils.add('buffereater') |
| write('for item in loop(%s):' % self.handle_node(node.seq)) |
| self.indention += 1 |
| write('yield item') |
| self.indention -= 1 |
| |
| write('context.pop()') |
| return '\n'.join(buf) |
| |
| def handle_if_condition(self, node): |
| """ |
| Handle an if condition node. |
| """ |
| buf = [] |
| write = lambda x: buf.append(self.indent(x)) |
| write(self.nodeinfo(node)) |
| for idx, (test, body) in enumerate(node.tests): |
| write('%sif %s:' % ( |
| idx and 'el' or '', |
| self.handle_node(test) |
| )) |
| self.indention += 1 |
| write(self.nodeinfo(body)) |
| buf.append(self.handle_node(body) or self.indent('pass')) |
| self.indention -= 1 |
| if node.else_ is not None: |
| write('else:') |
| self.indention += 1 |
| write(self.nodeinfo(node.else_)) |
| buf.append(self.handle_node(node.else_) or self.indent('pass')) |
| self.indention -= 1 |
| return '\n'.join(buf) |
| |
| def handle_cycle(self, node): |
| """ |
| Handle the cycle tag. |
| """ |
| self.used_data_structures.add('CycleContext') |
| name = '::cycle_%x' % self.last_cycle_id |
| self.last_cycle_id += 1 |
| buf = [] |
| write = lambda x: buf.append(self.indent(x)) |
| |
| write('if %r not in context.current:' % name) |
| self.indention += 1 |
| write(self.nodeinfo(node)) |
| if node.seq.__class__ in (nodes.TupleExpression, |
| nodes.ListExpression): |
| write('context.current[%r] = CycleContext(%s)' % ( |
| name, |
| self.to_tuple([self.handle_node(n) for n in node.seq.items]) |
| )) |
| hardcoded = True |
| else: |
| write('context.current[%r] = CycleContext()' % name) |
| hardcoded = False |
| self.indention -= 1 |
| |
| self.used_shortcuts.add('finish_var') |
| if hardcoded: |
| write('yield finish_var(context.current[%r].cycle(), ' |
| 'context)' % name) |
| else: |
| write('yield finish_var(context.current[%r].cycle(%s), ' |
| 'context)' % ( |
| name, |
| self.handle_node(node.seq) |
| )) |
| |
| return '\n'.join(buf) |
| |
| def handle_print(self, node): |
| """ |
| Handle a print statement. |
| """ |
| self.used_shortcuts.add('finish_var') |
| return self.indent(self.nodeinfo(node)) + '\n' +\ |
| self.indent('yield finish_var(%s, context)' % |
| self.handle_node(node.expr)) |
| |
| def handle_macro(self, node): |
| """ |
| Handle macro declarations. |
| """ |
| buf = [] |
| write = lambda x: buf.append(self.indent(x)) |
| |
| write('def macro(*args, **kw):') |
| self.indention += 1 |
| write(self.nodeinfo(node)) |
| |
| # collect macro arguments |
| arg_items = [] |
| caller_overridden = False |
| |
| # if we have conditional expressions available in that python |
| # build (for example cpython > 2.4) we can use them, they |
| # will perform slightly better. |
| if have_conditional_expr: |
| arg_tmpl = '\'%(name)s\': args[%(pos)d] if argcount > %(pos)d ' \ |
| 'else %(default)s' |
| # otherwise go with the and/or tuple hack: |
| else: |
| arg_tmpl = '\'%(name)s\': (argcount > %(pos)d and '\ |
| '(args[%(pos)d],) or (%(default)s,))[0]' |
| |
| if node.arguments: |
| varargs_init = '\'varargs\': args[%d:]' % len(node.arguments) |
| write('argcount = len(args)') |
| for idx, (name, n) in enumerate(node.arguments): |
| arg_items.append(arg_tmpl % { |
| 'name': name, |
| 'pos': idx, |
| 'default': n is None and 'undefined_singleton' or |
| self.handle_node(n) |
| }) |
| if name == 'caller': |
| caller_overridden = True |
| elif name == 'varargs': |
| varargs_init = None |
| else: |
| varargs_init = '\'varargs\': args' |
| |
| if caller_overridden: |
| write('kw.pop(\'caller\', None)') |
| else: |
| arg_items.append('\'caller\': kw.pop(\'caller\', undefined_singleton)') |
| if varargs_init: |
| arg_items.append(varargs_init) |
| |
| write('context.push({%s})' % ',\n '.join([ |
| idx and self.indent(item) or item for idx, item |
| in enumerate(arg_items) |
| ])) |
| |
| # disallow any keyword arguments |
| write('if kw:') |
| self.indention += 1 |
| write('raise TemplateRuntimeError(\'%s got an unexpected keyword ' |
| 'argument %%r\' %% iter(kw).next())' % node.name) |
| self.require_runtime_error = True |
| self.indention -= 1 |
| |
| write(self.nodeinfo(node.body)) |
| data = self.handle_node(node.body) |
| if data: |
| buf.append(data) |
| write('context.pop()') |
| write('if 0: yield None') |
| self.indention -= 1 |
| buf.append(self.indent('context[%r] = buffereater(macro, True)' % |
| node.name)) |
| self.used_utils.add('buffereater') |
| |
| return '\n'.join(buf) |
| |
| def handle_call(self, node): |
| """ |
| Handle extended macro calls. |
| """ |
| buf = [] |
| write = lambda x: buf.append(self.indent(x)) |
| |
| write('def call(**kwargs):') |
| self.indention += 1 |
| write('context.push(kwargs)') |
| data = self.handle_node(node.body) |
| if data: |
| buf.append(data) |
| write('context.pop()') |
| write('if 0: yield None') |
| self.indention -= 1 |
| write('yield ' + self.handle_call_expr(node.expr, |
| {'caller': 'buffereater(call)'})) |
| self.used_utils.add('buffereater') |
| |
| return '\n'.join(buf) |
| |
| def handle_set(self, node): |
| """ |
| Handle variable assignments. |
| """ |
| if node.scope_local: |
| tmpl = 'context[%r] = %s' |
| else: |
| tmpl = 'context.set_nonlocal(%r, %s)' |
| return self.indent(self.nodeinfo(node)) + '\n' + \ |
| self.indent(tmpl % ( |
| node.name, |
| self.handle_node(node.expr) |
| )) |
| |
| def handle_filter(self, node): |
| """ |
| Handle filter sections. |
| """ |
| buf = [] |
| write = lambda x: buf.append(self.indent(x)) |
| write('def filtered():') |
| self.indention += 1 |
| write('context.push()') |
| write(self.nodeinfo(node.body)) |
| data = self.handle_node(node.body) |
| if data: |
| buf.append(data) |
| write('context.pop()') |
| write('if 0: yield None') |
| self.indention -= 1 |
| self.used_shortcuts.add('apply_filters') |
| write('yield apply_filters(buffereater(filtered)(), context, %s)' % |
| self.to_tuple(['(%r, %s)' % ( |
| name, |
| self.to_tuple(map(self.handle_node, args)) |
| ) for name, args in node.filters]) |
| ) |
| self.used_utils.add('buffereater') |
| return '\n'.join(buf) |
| |
| def handle_block(self, node, level=0): |
| """ |
| Handle blocks in the sourcecode. We only use them to |
| call the current block implementation that is stored somewhere |
| else. |
| """ |
| rv = self.handle_node(node.body) |
| if not rv: |
| return '' |
| |
| self.used_data_structures.add('SuperBlock') |
| buf = [] |
| write = lambda x: buf.append(self.indent(x)) |
| |
| write(self.nodeinfo(node)) |
| write('context.push({\'super\': SuperBlock(%r, blocks, %r, context)})' % ( |
| str(node.name), |
| level |
| )) |
| write(self.nodeinfo(node.body)) |
| buf.append(rv) |
| write('context.pop()') |
| return '\n'.join(buf) |
| |
| def handle_include(self, node): |
| """ |
| Include another template at the current position. |
| """ |
| tmpl = self.loader.parse(node.template, |
| node.filename) |
| try: |
| return self.handle_node(tmpl.body) |
| finally: |
| self.loader.mark_as_processed() |
| |
| def handle_trans(self, node): |
| """ |
| Handle translations. |
| """ |
| if node.replacements: |
| replacements = [] |
| for name, n in node.replacements.iteritems(): |
| replacements.append('%r: %s' % ( |
| name, |
| self.handle_node(n) |
| )) |
| replacements = '{%s}' % ', '.join(replacements) |
| else: |
| replacements = 'None' |
| return self.indent(self.nodeinfo(node)) + '\n' +\ |
| self.indent('yield context.translate_func(%r, %r, %r, %s)' % ( |
| node.singular, |
| node.plural, |
| node.indicator, |
| replacements |
| )) |
| |
| # -- python nodes |
| |
| def handle_name(self, node): |
| """ |
| Handle name assignments and name retreivement. |
| """ |
| if node.name == '_': |
| return 'context.translate_func' |
| return 'context[%r]' % node.name |
| |
| def handle_compare(self, node): |
| """ |
| Any sort of comparison |
| """ |
| ops = { |
| 'eq': '==', |
| 'ne': '!=', |
| 'lt': '<', |
| 'lteq': '<=', |
| 'gt': '>', |
| 'gteq': '>=', |
| 'in': 'in', |
| 'not in': 'not in' |
| } |
| buf = [] |
| buf.append(self.handle_node(node.expr)) |
| for op, n in node.ops: |
| buf.append(ops[op]) |
| buf.append(self.handle_node(n)) |
| return ' '.join(buf) |
| |
| def handle_test(self, node): |
| """ |
| Handle test calls. |
| """ |
| self.used_shortcuts.add('perform_test') |
| return 'perform_test(context, %r, %s, %s)' % ( |
| node.name, |
| self.to_tuple([self.handle_node(n) for n in node.args]), |
| self.handle_node(node.node) |
| ) |
| |
| def handle_const(self, node): |
| """ |
| Constant values in expressions. |
| """ |
| return repr(node.value) |
| |
| def handle_regex(self, node): |
| """ |
| Regular expression literals. |
| """ |
| if self.environment.disable_regexps: |
| raise TemplateSyntaxError('regular expressions disabled.') |
| if node.value in self.compiled_regular_expressions: |
| return self.compiled_regular_expressions[node.value] |
| name = 'regex_%d' % len(self.compiled_regular_expressions) |
| self.compiled_regular_expressions[node.value] = name |
| return name |
| |
| def handle_subscript(self, node): |
| """ |
| Handle variable based attribute access foo['bar']. |
| """ |
| self.used_shortcuts.add('get_attribute') |
| if node.arg.__class__ is nodes.SliceExpression: |
| rv = self.handle_slice(node.arg, getslice_test=True) |
| if rv is not None: |
| return self.handle_node(node.node) + rv |
| return 'get_attribute(%s, %s)' % ( |
| self.handle_node(node.node), |
| self.handle_node(node.arg) |
| ) |
| |
| def handle_tuple(self, node): |
| """ |
| Tuple unpacking loops. |
| """ |
| return self.to_tuple([self.handle_node(n) for n in node.items]) |
| |
| def handle_filter_expr(self, node): |
| """ |
| We use the pipe operator for filtering. |
| """ |
| self.used_shortcuts.add('apply_filters') |
| return 'apply_filters(%s, context, %s)' % ( |
| self.handle_node(node.node), |
| self.to_tuple(['(%r, %s)' % ( |
| name, |
| self.to_tuple(map(self.handle_node, args)) |
| ) for name, args in node.filters]) |
| ) |
| |
| def handle_call_expr(self, node, extra_kwargs=None): |
| """ |
| Handle function calls. |
| """ |
| args = [] |
| kwargs = {} |
| dyn_args = dyn_kwargs = None |
| if node.dyn_args is not None: |
| dyn_args = self.handle_node(node.dyn_args) |
| if node.dyn_kwargs is not None: |
| dyn_kwargs = self.handle_node(node.dyn_kwargs) |
| for arg in node.args: |
| args.append(self.handle_node(arg)) |
| for name, arg in node.kwargs: |
| kwargs[name] = self.handle_node(arg) |
| if extra_kwargs: |
| kwargs.update(extra_kwargs) |
| if not (args or kwargs or dyn_args or dyn_kwargs): |
| self.used_shortcuts.add('call_function_simple') |
| return 'call_function_simple(%s, context)' % \ |
| self.handle_node(node.node) |
| self.used_shortcuts.add('call_function') |
| return 'call_function(%s, context, %s, {%s}, %s, %s)' % ( |
| self.handle_node(node.node), |
| self.to_tuple(args), |
| ', '.join(['%r: %s' % i for i in kwargs.iteritems()]), |
| dyn_args, |
| dyn_kwargs |
| ) |
| |
| def handle_concat(self, node): |
| """ |
| Convert some objects to unicode and concatenate them. |
| """ |
| self.used_shortcuts.add('to_unicode') |
| return "u''.join(%s)" % self.to_tuple([ |
| 'to_unicode(%s)' % self.handle_node(arg) |
| for arg in node.args |
| ]) |
| |
| |
| def handle_dict(self, node): |
| """ |
| Dict constructor syntax. |
| """ |
| return '{%s}' % ', '.join([ |
| '%s: %s' % ( |
| self.handle_node(key), |
| self.handle_node(value) |
| ) for key, value in node.items |
| ]) |
| |
| def handle_set_expr(self, node): |
| """ |
| Set constructor syntax. |
| """ |
| self.need_set_import = True |
| return 'set([%s])' % ', '.join([self.handle_node(n) |
| for n in node.items]) |
| |
| def handle_list(self, node): |
| """ |
| We don't know tuples, tuples are lists for jinja. |
| """ |
| return '[%s]' % ', '.join([ |
| self.handle_node(n) for n in node.items |
| ]) |
| |
| def handle_undefined(self, node): |
| """ |
| Return the current undefined literal. |
| """ |
| return 'undefined_singleton' |
| |
| |
| def handle_slice(self, node, getslice_test=False): |
| """ |
| Slice access. Because of backwards compatibilty to python's |
| `__getslice__` this function takes a second parameter that lets this |
| method return a regular slice bracket call. If a regular slice bracket |
| call that is compatible to __getslice__ is not possible the return |
| value will be `None` so that a regular `get_attribute` wrapping can |
| happen. |
| """ |
| if node.start is None: |
| start = not getslice_test and 'None' or '' |
| else: |
| start = self.handle_node(node.start) |
| if node.stop is None: |
| stop = not getslice_test and 'None' or '' |
| else: |
| stop = self.handle_node(node.stop) |
| if node.step is None: |
| step = 'None' |
| else: |
| if getslice_test: |
| return |
| step = self.handle_node(node.step) |
| if getslice_test: |
| return '[%s:%s]' % (start, stop) |
| return 'slice(%s, %s, %s)' % (start, stop, step) |
| |
| def handle_conditional_expr(self, node): |
| """ |
| Handle conditional expressions. |
| """ |
| if have_conditional_expr: |
| tmpl = '%(expr1)s if %(test)s else %(expr2)s' |
| else: |
| tmpl = '(%(test)s and (%(expr1)s,) or (%(expr2)s,))[0]' |
| return tmpl % { |
| 'test': self.handle_node(node.test), |
| 'expr1': self.handle_node(node.expr1), |
| 'expr2': self.handle_node(node.expr2) |
| } |