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:
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):
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):
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)
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)
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):
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
] + 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.
value = float(value)
elif token == 'operator':
token = operators[value]
- value = ''
yield Token(lineno, token, value)
return TokenStream(generate(), filename)
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
# 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
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
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
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,
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
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)
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):
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: