added support for new call statement
--HG--
branch : trunk
diff --git a/jinja2/compiler.py b/jinja2/compiler.py
index 63a140f..83e07e6 100644
--- a/jinja2/compiler.py
+++ b/jinja2/compiler.py
@@ -258,7 +258,7 @@
self.new_lines = 1
self._last_line = node.lineno
- def signature(self, node, frame, have_comma=True):
+ def signature(self, node, frame, have_comma=True, extra_kwargs=None):
have_comma = have_comma and [True] or []
def touch_comma():
if have_comma:
@@ -272,11 +272,16 @@
for kwarg in node.kwargs:
touch_comma()
self.visit(kwarg, frame)
+ if extra_kwargs is not None:
+ touch_comma()
+ self.write(extra_kwargs)
if node.dyn_args:
touch_comma()
+ self.write('*')
self.visit(node.dyn_args, frame)
if node.dyn_kwargs:
touch_comma()
+ self.write('**')
self.visit(node.dyn_kwargs, frame)
def pull_locals(self, frame, no_indent=False):
@@ -291,6 +296,48 @@
if not no_indent:
self.outdent()
+ def function_scoping(self, node, frame):
+ func_frame = frame.inner()
+ func_frame.inspect(node.iter_child_nodes(), hard_scope=True)
+
+ # variables that are undeclared (accessed before declaration) and
+ # declared locally *and* part of an outside scope raise a template
+ # assertion error. Reason: we can't generate reasonable code from
+ # it without aliasing all the variables. XXX: alias them ^^
+ overriden_closure_vars = (
+ func_frame.identifiers.undeclared &
+ func_frame.identifiers.declared &
+ (func_frame.identifiers.declared_locally |
+ func_frame.identifiers.declared_parameter)
+ )
+ if overriden_closure_vars:
+ vars = ', '.join(sorted(overriden_closure_vars))
+ raise TemplateAssertionError('It\'s not possible to set and '
+ 'access variables derived from '
+ 'an outer scope! (affects: %s' %
+ vars, node.lineno, self.filename)
+
+ # remove variables from a closure from the frame's undeclared
+ # identifiers.
+ func_frame.identifiers.undeclared -= (
+ func_frame.identifiers.undeclared &
+ func_frame.identifiers.declared
+ )
+
+ func_frame.accesses_arguments = False
+ func_frame.accesses_caller = False
+ func_frame.arguments = args = ['l_' + x.name for x in node.args]
+
+ if 'arguments' in func_frame.identifiers.undeclared:
+ func_frame.accesses_arguments = True
+ func_frame.identifiers.add_special('arguments')
+ args.append('l_arguments')
+ if 'caller' in func_frame.identifiers.undeclared:
+ func_frame.accesses_caller = True
+ func_frame.identifiers.add_special('caller')
+ args.append('l_caller')
+ return func_frame
+
# -- Visitors
def visit_Template(self, node, frame=None):
@@ -489,45 +536,11 @@
self.blockvisit(node.else_, if_frame)
def visit_Macro(self, node, frame):
- macro_frame = frame.inner()
- macro_frame.inspect(node.iter_child_nodes(), hard_scope=True)
-
- # variables that are undeclared (accessed before declaration) and
- # declared locally *and* part of an outside scope raise a template
- # assertion error. Reason: we can't generate reasonable code from
- # it without aliasing all the variables. XXX: alias them ^^
- overriden_closure_vars = (
- macro_frame.identifiers.undeclared &
- macro_frame.identifiers.declared &
- (macro_frame.identifiers.declared_locally |
- macro_frame.identifiers.declared_parameter)
- )
- if overriden_closure_vars:
- vars = ', '.join(sorted(overriden_closure_vars))
- raise TemplateAssertionError('It\'s not possible to set and '
- 'access variables derived from '
- 'an outer scope! (affects: %s' %
- vars, node.lineno, self.filename)
-
- # remove variables from a closure from the frame's undeclared
- # identifiers.
- macro_frame.identifiers.undeclared -= (
- macro_frame.identifiers.undeclared &
- macro_frame.identifiers.declared
- )
-
- args = ['l_' + x.name for x in node.args]
- if 'arguments' in macro_frame.identifiers.undeclared:
- accesses_arguments = True
- args.append('l_arguments')
- else:
- accesses_arguments = False
+ macro_frame = self.function_scoping(node, frame)
+ args = macro_frame.arguments
self.writeline('def macro(%s):' % ', '.join(args), node)
- self.indent()
- self.writeline('if 0: yield None')
- self.outdent()
self.pull_locals(macro_frame)
- self.blockvisit(node.body, macro_frame)
+ self.blockvisit(node.body, macro_frame, True)
self.newline()
if frame.toplevel:
self.write('context[%r] = ' % node.name)
@@ -539,7 +552,26 @@
for arg in node.defaults:
self.visit(arg)
self.write(', ')
- self.write('), %r)' % accesses_arguments)
+ self.write('), %r, %r)' % (
+ macro_frame.accesses_arguments,
+ macro_frame.accesses_caller
+ ))
+
+ def visit_CallBlock(self, node, frame):
+ call_frame = self.function_scoping(node, frame)
+ args = call_frame.arguments
+ self.writeline('def call(%s):' % ', '.join(args), node)
+ self.blockvisit(node.body, call_frame, node)
+ arg_tuple = ', '.join(repr(x.name) for x in node.args)
+ if len(node.args) == 1:
+ arg_tuple += ','
+ self.writeline('caller = Macro(call, None, (%s), (' % arg_tuple)
+ for arg in node.defaults:
+ self.visit(arg)
+ self.write(', ')
+ self.write('), %r, False)' % call_frame.accesses_arguments)
+ self.writeline('yield ', node)
+ self.visit_Call(node.call, call_frame, extra_kwargs='caller=caller')
def visit_ExprStmt(self, node, frame):
self.newline(node)
@@ -770,10 +802,10 @@
self.signature(node, frame)
self.write(')')
- def visit_Call(self, node, frame):
+ def visit_Call(self, node, frame, extra_kwargs=None):
self.visit(node.node, frame)
self.write('(')
- self.signature(node, frame, False)
+ self.signature(node, frame, False, extra_kwargs)
self.write(')')
def visit_Keyword(self, node, frame):
diff --git a/jinja2/lexer.py b/jinja2/lexer.py
index 5d8ab7b..1f033d7 100644
--- a/jinja2/lexer.py
+++ b/jinja2/lexer.py
@@ -33,7 +33,6 @@
integer_re = re.compile(r'\d+')
name_re = re.compile(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b')
float_re = re.compile(r'\d+\.\d+')
-eol_re = re.compile(r'(\s*$\s*)+(?m)')
# set of used keywords
@@ -340,9 +339,8 @@
] + tag_rules
def tokenize(self, source, filename=None):
- """
- Works like `tokeniter` but returns a tokenstream of tokens and not a
- generator or token tuples. Additionally all token values are already
+ """Works like `tokeniter` but returns a tokenstream of tokens and not
+ a generator or token tuples. Additionally all token values are already
converted into types and postprocessed. For example keywords are
already keyword tokens, not named tokens, comments are removed,
integers and floats converted, strings unescaped etc.
@@ -377,7 +375,6 @@
value = float(value)
elif token == 'operator':
token = operators[value]
- value = ''
yield Token(lineno, token, value)
return TokenStream(generate(), filename)
@@ -398,12 +395,12 @@
balancing_stack = []
- while True:
+ while 1:
# tokenizer loop
for regex, tokens, new_state in statetokens:
m = regex.match(source, pos)
# if no match we try again with the next rule
- if not m:
+ if m is None:
continue
# we only match blocks and variables if brances / parentheses
@@ -411,7 +408,8 @@
# is the operator rule. do this only if the end tags look
# like operators
if balancing_stack and \
- tokens in ('variable_end', 'block_end'):
+ tokens in ('variable_end', 'block_end',
+ 'linestatement_end'):
continue
# tuples support more options
@@ -447,8 +445,7 @@
yield lineno, token, data
lineno += data.count('\n')
- # strings as token just are yielded as it, but just
- # if the data is not empty
+ # strings as token just are yielded as it.
else:
data = m.group()
# update brace/parentheses balance
@@ -472,8 +469,7 @@
lineno, filename)
# yield items
if tokens is not None:
- if data:
- yield lineno, tokens, data
+ yield lineno, tokens, data
lineno += data.count('\n')
# fetch new position into new variable so that we can check
diff --git a/jinja2/nodes.py b/jinja2/nodes.py
index 00f6d62..b569a31 100644
--- a/jinja2/nodes.py
+++ b/jinja2/nodes.py
@@ -222,7 +222,7 @@
class CallBlock(Stmt):
"""A node that represents am extended macro call."""
- fields = ('call', 'body')
+ fields = ('call', 'args', 'defaults', 'body')
class Set(Stmt):
diff --git a/jinja2/parser.py b/jinja2/parser.py
index 74660f6..81c21ee 100644
--- a/jinja2/parser.py
+++ b/jinja2/parser.py
@@ -175,8 +175,30 @@
self.end_statement()
return node
+ def parse_signature(self, node):
+ node.args = args = []
+ node.defaults = defaults = []
+ self.stream.expect('lparen')
+ while self.stream.current.type is not 'rparen':
+ if args:
+ self.stream.expect('comma')
+ token = self.stream.expect('name')
+ arg = nodes.Name(token.value, 'param', lineno=token.lineno)
+ if not arg.can_assign():
+ raise TemplateSyntaxError("can't assign to '%s'" %
+ arg.name, arg.lineno,
+ self.filename)
+ if self.stream.current.type is 'assign':
+ self.stream.next()
+ defaults.append(self.parse_expression())
+ args.append(arg)
+ self.stream.expect('rparen')
+
def parse_call_block(self):
node = nodes.CallBlock(lineno=self.stream.expect('call').lineno)
+ if self.stream.current.type is 'lparen':
+ self.parse_signature(node)
+
node.call = self.parse_expression()
if not isinstance(node.call, nodes.Call):
raise TemplateSyntaxError('expected call', node.lineno,
@@ -192,23 +214,7 @@
raise TemplateSyntaxError('can\'t assign macro to %r' %
node.target, node.lineno,
self.filename)
- self.stream.expect('lparen')
- node.args = args = []
- node.defaults = defaults = []
- while self.stream.current.type is not 'rparen':
- if args:
- self.stream.expect('comma')
- token = self.stream.expect('name')
- arg = nodes.Name(token.value, 'param', lineno=token.lineno)
- if not arg.can_assign():
- raise TemplateSyntaxError("can't assign to '%s'" %
- arg.name, arg.lineno,
- self.filename)
- if self.stream.current.type is 'assign':
- self.stream.next()
- defaults.append(self.parse_expression())
- args.append(arg)
- self.stream.expect('rparen')
+ self.parse_signature(node)
node.body = self.parse_statements(('endmacro',), drop_needle=True)
return node
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index 1bc196e..238d4cf 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -193,12 +193,13 @@
class Macro(object):
"""Wraps a macro."""
- def __init__(self, func, name, arguments, defaults, catch_all):
- self.func = func
+ def __init__(self, func, name, arguments, defaults, catch_all, caller):
+ self._func = func
self.name = name
self.arguments = arguments
self.defaults = defaults
self.catch_all = catch_all
+ self.caller = caller
def __call__(self, *args, **kwargs):
arg_count = len(self.arguments)
@@ -218,9 +219,20 @@
except IndexError:
value = Undefined(name)
arguments['l_' + name] = value
+ if self.caller:
+ caller = kwargs.pop('caller', None)
+ if caller is None:
+ caller = Undefined('caller')
+ arguments['l_caller'] = caller
if self.catch_all:
arguments['l_arguments'] = kwargs
- return TemplateData(u''.join(self.func(**arguments)))
+ return TemplateData(u''.join(self._func(**arguments)))
+
+ def __repr__(self):
+ return '<%s %s>' % (
+ self.__class__.__name__,
+ self.name is None and 'anonymous' or repr(self.name)
+ )
class Undefined(object):
@@ -228,7 +240,7 @@
def __init__(self, name=None, attr=None):
if attr is None:
- self._undefined_hint = '%r is undefined' % attr
+ self._undefined_hint = '%r is undefined' % name
elif name is None:
self._undefined_hint = 'attribute %r is undefined' % name
else: