Skip to content

Commit

Permalink
revamped jinja2 import system. the behavior is less confusing now, bu…
Browse files Browse the repository at this point in the history
…t it's not backwards compatible. I like it though ;)

--HG--
branch : trunk
  • Loading branch information
mitsuhiko committed Apr 25, 2008
1 parent 6ce170c commit 0611e49
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 60 deletions.
74 changes: 42 additions & 32 deletions jinja2/compiler.py
Expand Up @@ -210,14 +210,15 @@ def visit_Test(self, 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."""
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."""
Expand All @@ -232,7 +233,8 @@ def visit_Assign(self, node):
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):
Expand Down Expand Up @@ -597,40 +599,45 @@ def visit_Extends(self, node, frame):

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')
else:
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()
Expand Down Expand Up @@ -1022,6 +1029,9 @@ def visit_Slice(self, node, frame):
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):
Expand All @@ -1037,9 +1047,9 @@ def visit_Filter(self, node, frame, initial=None):

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(')')
Expand Down
3 changes: 2 additions & 1 deletion jinja2/filters.py
Expand Up @@ -633,5 +633,6 @@ class _GroupTuple(tuple):
'round': do_round,
'sort': do_sort,
'groupby': do_groupby,
'safe': Markup
'safe': Markup,
'xmlattr': do_xmlattr
}
3 changes: 2 additions & 1 deletion jinja2/lexer.py
Expand Up @@ -37,7 +37,8 @@
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 = {
Expand Down
17 changes: 17 additions & 0 deletions jinja2/nodes.py
Expand Up @@ -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')
Expand Down
11 changes: 11 additions & 0 deletions jinja2/optimizer.py
Expand Up @@ -173,6 +173,17 @@ def walk(target, value):
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)
Expand Down
62 changes: 44 additions & 18 deletions jinja2/parser.py
Expand Up @@ -14,7 +14,7 @@


_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
Expand Down Expand Up @@ -145,22 +145,47 @@ def parse_extends(self):

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):
Expand Down Expand Up @@ -568,8 +593,9 @@ def ensure(expr):
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())
Expand Down
9 changes: 7 additions & 2 deletions jinja2/runtime.py
Expand Up @@ -14,7 +14,11 @@


__all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext',
'Macro', 'Markup']
'Macro', 'Markup', 'missing']


# special singleton representing missing values for the runtime
missing = object()


class TemplateContext(object):
Expand Down Expand Up @@ -69,7 +73,8 @@ def update(self, mapping):

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."""
Expand Down
6 changes: 0 additions & 6 deletions tests/test_syntax.py
Expand Up @@ -14,7 +14,6 @@
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 }}'''
Expand Down Expand Up @@ -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)'
Expand Down

0 comments on commit 0611e49

Please sign in to comment.