From 0611e49bb983fcefc3defd360c962f18ddb8b84f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 25 Apr 2008 23:44:14 +0200 Subject: [PATCH] revamped jinja2 import system. the behavior is less confusing now, but it's not backwards compatible. I like it though ;) --HG-- branch : trunk --- jinja2/compiler.py | 74 +++++++++++++++++++++++++------------------- jinja2/filters.py | 3 +- jinja2/lexer.py | 3 +- jinja2/nodes.py | 17 ++++++++++ jinja2/optimizer.py | 11 +++++++ jinja2/parser.py | 62 ++++++++++++++++++++++++++----------- jinja2/runtime.py | 9 ++++-- tests/test_syntax.py | 6 ---- 8 files changed, 125 insertions(+), 60 deletions(-) diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 542beed1..958b2c3e 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -210,14 +210,15 @@ class FrameIdentifierVisitor(NodeVisitor): 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.""" + def visit_Import(self, node): self.generic_visit(node) - if node.target is not None: - self.identifiers.declared_locally.add(node.target) + self.identifiers.declared_locally.add(node.target) + + def visit_FromImport(self, node): + self.generic_visit(node) + self.identifiers.declared_locally.update(node.names) def visit_Assign(self, node): """Visit assignments in the correct order.""" @@ -232,7 +233,8 @@ class FrameIdentifierVisitor(NodeVisitor): class CompilerExit(Exception): """Raised if the compiler encountered a situation where it just doesn't make sense to further process the code. Any block that - raises such an exception is not further processed.""" + raises such an exception is not further processed. + """ class CodeGenerator(NodeVisitor): @@ -597,28 +599,11 @@ class CodeGenerator(NodeVisitor): 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('environment.get_template(') - self.visit(node.template, frame) - self.write(', %r).include(context)' % self.name) - return - self.writeline('included_template = environment.get_template(', node) self.visit(node.template, frame) self.write(', %r)' % self.name) - if frame.toplevel: - self.writeline('included_context = included_template.new_context(' - 'context.get_root())') - self.writeline('for event in included_template.root_render_func(' - 'included_context):') - else: - self.writeline('for event in included_template.root_render_func(' - 'included_template.new_context(context.get_root())):') + self.writeline('for event in included_template.root_render_func(' + 'included_template.new_context(context.get_root())):') self.indent() if frame.buffer is None: self.writeline('yield event') @@ -626,11 +611,33 @@ class CodeGenerator(NodeVisitor): self.writeline('%s.append(event)' % frame.buffer) 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 + def visit_Import(self, node, frame): + """Visit regular imports.""" + self.writeline('l_%s = ' % node.target, node) if frame.toplevel: - self.writeline('context.update(included_context.get_exported())') + self.write('context[%r] = ' % node.target) + self.write('environment.get_template(') + self.visit(node.template, frame) + self.write(', %r).include(context)' % self.name) + + def visit_FromImport(self, node, frame): + """Visit named imports.""" + self.newline(node) + self.write('included_template = environment.get_template(') + self.visit(node.template, frame) + self.write(', %r).include(context)' % self.name) + for name in node.names: + self.writeline('l_%s = getattr(included_template, ' + '%r, missing)' % (name, name)) + self.writeline('if l_%s is missing:' % name) + self.indent() + self.writeline('l_%s = environment.undefined(%r %% ' + 'included_template.name)' % + (name, 'the template %r does not export ' + 'the requested name ' + repr(name))) + self.outdent() + if frame.toplevel: + self.writeline('context[%r] = l_%s' % (name, name)) def visit_For(self, node, frame): loop_frame = frame.inner() @@ -1022,6 +1029,9 @@ class CodeGenerator(NodeVisitor): def visit_Filter(self, node, frame, initial=None): self.write('f_%s(' % node.name) func = self.environment.filters.get(node.name) + if func is None: + raise TemplateAssertionError('no filter named %r' % node.name, + node.lineno, self.filename) if getattr(func, 'contextfilter', False): self.write('context, ') elif getattr(func, 'environmentfilter', False): @@ -1037,9 +1047,9 @@ class CodeGenerator(NodeVisitor): def visit_Test(self, node, frame): self.write('t_%s(' % node.name) - func = self.environment.tests.get(node.name) - if getattr(func, 'contexttest', False): - self.write('context, ') + if node.name not in self.environment.tests: + raise TemplateAssertionError('no test named %r' % node.name, + node.lineno, self.filename) self.visit(node.node, frame) self.signature(node, frame) self.write(')') diff --git a/jinja2/filters.py b/jinja2/filters.py index 68f9b5f2..801b3502 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -633,5 +633,6 @@ FILTERS = { 'round': do_round, 'sort': do_sort, 'groupby': do_groupby, - 'safe': Markup + 'safe': Markup, + 'xmlattr': do_xmlattr } diff --git a/jinja2/lexer.py b/jinja2/lexer.py index 5217c7d8..772dee27 100644 --- a/jinja2/lexer.py +++ b/jinja2/lexer.py @@ -37,7 +37,8 @@ float_re = re.compile(r'\d+\.\d+') keywords = set(['and', 'block', 'elif', 'else', 'endblock', 'print', 'endfilter', 'endfor', 'endif', 'endmacro', 'endraw', 'extends', 'filter', 'for', 'if', 'in', 'include', - 'is', 'macro', 'not', 'or', 'raw', 'call', 'endcall']) + 'is', 'macro', 'not', 'or', 'raw', 'call', 'endcall', + 'from', 'import']) # bind operators to token types operators = { diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 5f3aabb0..3aed350e 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -263,9 +263,26 @@ class Block(Stmt): class Include(Stmt): """A node that represents the include tag.""" + fields = ('template',) + + +class Import(Stmt): + """A node that represents the import tag.""" fields = ('template', 'target') +class FromImport(Stmt): + """A node that represents the from import tag. It's important to not + pass unsafe names to the name attribute. The compiler translates the + attribute lookups directly into getattr calls and does *not* use the + subscribe callback of the interface. As exported variables may not + start with double underscores (which the parser asserts) this is not a + problem for regular Jinja code, but if this node is used in an extension + extra care must be taken. + """ + fields = ('template', 'names') + + class Trans(Stmt): """A node for translatable sections.""" fields = ('singular', 'plural', 'indicator', 'replacements') diff --git a/jinja2/optimizer.py b/jinja2/optimizer.py index 4d7b9f53..c432b3b1 100644 --- a/jinja2/optimizer.py +++ b/jinja2/optimizer.py @@ -173,6 +173,17 @@ class Optimizer(NodeTransformer): return node return result + def visit_Import(self, node, context): + rv = self.generic_visit(node, context) + context.undef(node.target) + return rv + + def visit_FromImport(self, node, context): + rv = self.generic_visit(node, context) + for name in node.names: + context.undef(name) + return rv + def fold(self, node, context): """Do constant folding.""" node = self.generic_visit(node, context) diff --git a/jinja2/parser.py b/jinja2/parser.py index 8db62dea..daa7a0d3 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -14,7 +14,7 @@ from jinja2.exceptions import TemplateSyntaxError _statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print', - 'macro', 'include']) + 'macro', 'include', 'from', 'import']) _compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq', 'in']) statement_end_tokens = set(['variable_end', 'block_end', 'in']) _tuple_edge_tokens = set(['rparen']) | statement_end_tokens @@ -145,22 +145,47 @@ class Parser(object): def parse_include(self): node = nodes.Include(lineno=self.stream.expect('include').lineno) - expr = self.parse_expression() - if self.stream.current.type is 'assign': + node.template = self.parse_expression() + return node + + def parse_import(self): + node = nodes.Import(lineno=self.stream.expect('import').lineno) + node.template = self.parse_expression() + self.stream.expect('name:as') + node.target = self.stream.expect('name').value + if not nodes.Name(node.target, 'store').can_assign(): + raise TemplateSyntaxError('can\'t assign imported template ' + 'to %r' % node.target, node.lineno, + self.filename) + return node + + def parse_from(self): + node = nodes.FromImport(lineno=self.stream.expect('from').lineno) + node.template = self.parse_expression() + self.stream.expect('import') + node.names = [] + while 1: + if node.names: + self.stream.expect('comma') + if self.stream.current.type is 'name': + target = nodes.Name(self.stream.current.value, 'store') + if not target.can_assign(): + raise TemplateSyntaxError('can\'t import object named %r' + % target.name, target.lineno, + self.filename) + elif target.name.startswith('__'): + raise TemplateAssertionError('names starting with two ' + 'underscores can not be ' + 'imported', target.lineno, + self.filename) + node.names.append(target.name) + self.stream.next() + if self.stream.current.type is not 'comma': + break + else: + break + if self.stream.current.type is 'comma': self.stream.next() - if not isinstance(expr, nodes.Name): - raise TemplateSyntaxError('must assign imported template to ' - 'variable or current scope', - expr.lineno, self.filename) - if not expr.can_assign(): - raise TemplateSyntaxError('can\'t assign imported template ' - 'to %r' % expr, expr.lineno, - self.filename) - node.target = expr.name - node.template = self.parse_expression() - else: - node.target = None - node.template = expr return node def parse_signature(self, node): @@ -568,8 +593,9 @@ class Parser(object): self.stream.look().type is 'assign': key = self.stream.current.value self.stream.skip(2) - kwargs.append(nodes.Keyword(key, self.parse_expression(), - lineno=key.lineno)) + value = self.parse_expression() + kwargs.append(nodes.Keyword(key, value, + lineno=value.lineno)) else: ensure(not kwargs) args.append(self.parse_expression()) diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 6b9abbd6..7860dcc0 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -14,7 +14,11 @@ from jinja2.exceptions import UndefinedError __all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext', - 'Macro', 'Markup'] + 'Macro', 'Markup', 'missing'] + + +# special singleton representing missing values for the runtime +missing = object() class TemplateContext(object): @@ -69,7 +73,8 @@ class TemplateContext(object): def get_exported(self): """Get a new dict with the exported variables.""" - return dict((k, self.vars[k]) for k in self.exported_vars) + return dict((k, self.vars[k]) for k in self.exported_vars + if not k.startswith('__')) def get_root(self): """Return a new dict with all the non local variables.""" diff --git a/tests/test_syntax.py b/tests/test_syntax.py index dc91990e..af2b0f59 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -14,7 +14,6 @@ CALL = '''{{ foo('a', c='d', e='f', *['b'], **{'g': 'h'}) }}''' SLICING = '''{{ [1, 2, 3][:] }}|{{ [1, 2, 3][::-1] }}''' ATTR = '''{{ foo.bar }}|{{ foo['bar'] }}''' SUBSCRIPT = '''{{ foo[0] }}|{{ foo[-1] }}''' -KEYATTR = '''{{ {'items': 'foo'}.items }}|{{ {}.items() }}''' TUPLE = '''{{ () }}|{{ (1,) }}|{{ (1, 2) }}''' MATH = '''{{ (1 + 1 * 2) - 3 / 2 }}|{{ 2**3 }}''' DIV = '''{{ 3 // 2 }}|{{ 3 / 2 }}|{{ 3 % 2 }}''' @@ -64,11 +63,6 @@ def test_subscript(env): assert tmpl.render(foo=[0, 1, 2]) == '0|2' -def test_keyattr(env): - tmpl = env.from_string(KEYATTR) - assert tmpl.render() == 'foo|[]' - - def test_tuple(env): tmpl = env.from_string(TUPLE) assert tmpl.render() == '()|(1,)|(1, 2)' -- 2.47.2