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."""
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):
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')
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()
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):
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(')')
'round': do_round,
'sort': do_sort,
'groupby': do_groupby,
- 'safe': Markup
+ 'safe': Markup,
+ 'xmlattr': do_xmlattr
}
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 = {
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')
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)
_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
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):
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())
__all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext',
- 'Macro', 'Markup']
+ 'Macro', 'Markup', 'missing']
+
+
+# special singleton representing missing values for the runtime
+missing = object()
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."""
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 }}'''
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)'