From f059ec1f41abd0e069bd2100c806dd3014724ab9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 11 Apr 2008 22:21:00 +0200 Subject: [PATCH] implemented includes --HG-- branch : trunk --- jinja2/compiler.py | 76 ++++++++++++++++++++++++++++++++++++------- jinja2/environment.py | 5 ++- jinja2/parser.py | 13 +++++--- jinja2/runtime.py | 41 +++++++++++++++++++---- test.py | 5 +++ 5 files changed, 118 insertions(+), 22 deletions(-) diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 74cd0a5..63a140f 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -79,8 +79,9 @@ class Identifiers(object): # names that are declared by parameters self.declared_parameter = set() - # filters that are referenced + # filters/tests that are referenced self.filters = set() + self.tests = set() def add_special(self, name): """Register a special name like `loop`.""" @@ -166,13 +167,22 @@ class FrameIdentifierVisitor(NodeVisitor): def visit_Filter(self, node): self.generic_visit(node) - if node.name not in self.identifiers.filters: - self.identifiers.filters.add(node.name) + self.identifiers.filters.add(node.name) + + def visit_Test(self, node): + self.generic_visit(node) + self.identifiers.tests.add(node.name) def visit_Macro(self, node): """Macros set local.""" self.identifiers.declared_locally.add(node.name) + def visit_Include(self, node): + """Some includes set local.""" + self.generic_visit(node) + if node.target is not None: + self.identifiers.declared_locally.add(node.target) + def visit_Assign(self, node): """Visit assignments in the correct order.""" self.visit(node.node) @@ -276,6 +286,8 @@ class CodeGenerator(NodeVisitor): self.writeline('l_%s = context[%r]' % (name, name)) for name in frame.identifiers.filters: self.writeline('f_%s = environment.filters[%r]' % (name, name)) + for name in frame.identifiers.tests: + self.writeline('t_%s = environment.tests[%r]' % (name, name)) if not no_indent: self.outdent() @@ -299,10 +311,11 @@ class CodeGenerator(NodeVisitor): self.blocks[block.name] = block # generate the root render function. - self.writeline('def root(globals, environment=environment):', extra=1) + self.writeline('def root(globals, environment=environment' + ', standalone=False):', extra=1) self.indent() - self.writeline('context = TemplateContext(globals, %r, blocks)' % - self.filename) + self.writeline('context = TemplateContext(globals, %r, blocks' + ', standalone)' % self.filename) if have_extends: self.writeline('parent_root = None') self.outdent() @@ -312,7 +325,10 @@ class CodeGenerator(NodeVisitor): frame.inspect(node.body) frame.toplevel = frame.rootlevel = True self.pull_locals(frame) - self.blockvisit(node.body, frame, True) + self.indent() + self.writeline('yield context') + self.outdent() + self.blockvisit(node.body, frame) # make sure that the parent root is called. if have_extends: @@ -320,7 +336,9 @@ class CodeGenerator(NodeVisitor): self.indent() self.writeline('if parent_root is not None:') self.indent() - self.writeline('for event in parent_root(context):') + self.writeline('stream = parent_root(context)') + self.writeline('stream.next()') + self.writeline('for event in stream:') self.indent() self.writeline('yield event') self.outdent(1 + self.has_known_extends) @@ -395,6 +413,34 @@ class CodeGenerator(NodeVisitor): # and now we have one more self.extends_so_far += 1 + def visit_Include(self, node, frame): + """Handles includes.""" + # simpled include is include into a variable. This kind of + # include works the same on every level, so we handle it first. + if node.target is not None: + self.writeline('l_%s = ' % node.target, node) + if frame.toplevel: + self.write('context[%r] = ' % node.target) + self.write('IncludedTemplate(environment, context, ') + self.visit(node.template, frame) + self.write(')') + return + + self.writeline('included_stream = environment.get_template(', node) + self.visit(node.template, frame) + self.write(').root_render_func(context, standalone=True)') + self.writeline('included_context = included_stream.next()') + self.writeline('for event in included_stream:') + self.indent() + self.writeline('yield event') + self.outdent() + + # if we have a toplevel include the exported variables are copied + # into the current context without exporting them. context.udpate + # does *not* mark the variables as exported + if frame.toplevel: + self.writeline('context.update(included_context.get_exported())') + def visit_For(self, node, frame): loop_frame = frame.inner() loop_frame.inspect(node.iter_child_nodes()) @@ -507,8 +553,10 @@ class CodeGenerator(NodeVisitor): self.newline(node) if self.environment.finalize is unicode: finalizer = 'unicode' + have_finalizer = False else: finalizer = 'context.finalize' + have_finalizer = False # if we are in the toplevel scope and there was already an extends # statement we have to add a check that disables our yield(s) here @@ -559,10 +607,10 @@ class CodeGenerator(NodeVisitor): for idx, argument in enumerate(arguments): if idx: self.write(', ') - if finalizer != 'unicode': + if have_finalizer: self.write('(') self.visit(argument, frame) - if finalizer != 'unicode': + if have_finalizer: self.write(')') self.write(idx == 0 and ',)' or ')') @@ -706,12 +754,18 @@ class CodeGenerator(NodeVisitor): def visit_Filter(self, node, frame): self.write('f_%s(' % node.name) + func = self.environment.filters.get(node.name) + if getattr(func, 'contextfilter', False): + self.write('context, ') self.visit(node.node, frame) self.signature(node, frame) self.write(')') def visit_Test(self, node, frame): - self.write('environment.tests[%r](') + self.write('t_%s(' % node.name) + func = self.environment.tests.get(node.name) + if getattr(func, 'contexttest', False): + self.write('context, ') self.visit(node.node, frame) self.signature(node, frame) self.write(')') diff --git a/jinja2/environment.py b/jinja2/environment.py index 417a035..8d5e9d4 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -145,4 +145,7 @@ class Template(object): return u''.join(self.stream(*args, **kwargs)) def stream(self, *args, **kwargs): - return self.root_render_func(dict(*args, **kwargs)) + gen = self.root_render_func(dict(*args, **kwargs)) + # skip the first item which is a reference to the stream + gen.next() + return gen diff --git a/jinja2/parser.py b/jinja2/parser.py index 8dc5d58..f1195c4 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -161,12 +161,12 @@ class Parser(object): expr = self.parse_expression() if self.stream.current.type is 'assign': self.stream.next() - if not expr.can_assign(): + node.target = self.stream.expect('name') + # make sure that assignments to that name are allowed + if not nodes.Name(node.target, 'store').can_assign(): raise TemplateSyntaxError('can\'t assign imported template ' - 'to this expression.', expr.lineno, + 'to %r' % node.target, expr.lineno, self.filename) - expr.set_ctx('store') - node.target = expr node.template = self.parse_expression() else: node.target = None @@ -186,6 +186,11 @@ class Parser(object): def parse_macro(self): node = nodes.Macro(lineno=self.stream.expect('macro').lineno) node.name = self.stream.expect('name').value + # make sure that assignments to that name are allowed + if not nodes.Name(node.name, 'store').can_assign(): + raise TemplateSyntaxError('can\'t assign macro to %r' % + node.target, node.lineno, + self.filename) self.stream.expect('lparen') node.args = args = [] node.defaults = defaults = [] diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 27dab9f..fcd537d 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -14,8 +14,8 @@ except ImportError: defaultdict = None -__all__ = ['subscribe', 'LoopContext', 'StaticLoopContext', - 'TemplateContext', 'Macro', 'Undefined'] +__all__ = ['subscribe', 'LoopContext', 'StaticLoopContext', 'TemplateContext', + 'Macro', 'IncludedTemplate', 'Undefined'] def subscribe(obj, argument): @@ -45,14 +45,19 @@ class TemplateContext(dict): the exported variables for example). """ - def __init__(self, globals, filename, blocks): + def __init__(self, globals, filename, blocks, standalone): dict.__init__(self, globals) self.exported = set() self.filename = filename self.blocks = dict((k, [v]) for k, v in blocks.iteritems()) - if isinstance(globals, TemplateContext): - for name, parent_blocks in globals.blocks.iteritems(): - self.blocks.setdefault(name, []).extend(parent_blocks) + + # if the template is in standalone mode we don't copy the blocks over. + # this is used for includes for example but otherwise, if the globals + # are a template context, this template is participating in a template + # inheritance chain and we have to copy the blocks over. + if not standalone and isinstance(globals, TemplateContext): + for name, parent_blocks in globals.blocks.iteritems(): + self.blocks.setdefault(name, []).extend(parent_blocks) def __setitem__(self, key, value): """If we set items to the dict we track the variables set so @@ -79,6 +84,29 @@ class TemplateContext(dict): def __missing__(self, key): return Undefined(key) + def __repr__(self): + return '<%s %s of %r>' % ( + self.__class__.__name__, + dict.__repr__(self), + self.filename + ) + + +class IncludedTemplate(object): + """Represents an included template.""" + + def __init__(self, environment, context, template): + root = environment.get_template(template).root_render_func + gen = root(context, standalone=True) + self._context = gen.next().get_exported() + self._rendered_body = u''.join(gen) + + def __getitem__(self, name): + return self._context[name] + + def __unicode__(self): + return self._context + class LoopContextBase(object): """Helper for extended iteration.""" @@ -98,6 +126,7 @@ class LoopContextBase(object): class LoopContext(LoopContextBase): + """A loop context for dynamic iteration.""" def __init__(self, iterable, parent=None, enforce_length=False): self._iterable = iterable diff --git a/test.py b/test.py index 549efe8..45a5e65 100644 --- a/test.py +++ b/test.py @@ -4,15 +4,20 @@ from jinja2.loaders import DictLoader env = Environment(loader=DictLoader({ 'child.html': u'''\ {% extends master_layout or 'master.html' %} +{% include 'helpers.html' %} {% macro get_the_answer() %}42{% endmacro %} {% block body %} {{ get_the_answer() }} + {{ conspirate() }} {% endblock %} ''', 'master.html': u'''\ Foo {% block body %}{% endblock %} +''', +'helpers.html': u'''\ +{% macro conspirate() %}23{% endmacro %} ''' })) -- 2.26.2