From 71082079071b921df02974898f08ad433be70aca Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 12 Apr 2008 14:19:36 +0200 Subject: [PATCH] added support for new call statement --HG-- branch : trunk --- jinja2/compiler.py | 114 +++++++++++++++++++++++++++++---------------- jinja2/lexer.py | 20 ++++---- jinja2/nodes.py | 2 +- jinja2/parser.py | 40 +++++++++------- jinja2/runtime.py | 20 ++++++-- 5 files changed, 121 insertions(+), 75 deletions(-) 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 @@ class CodeGenerator(NodeVisitor): 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 @@ class CodeGenerator(NodeVisitor): 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 @@ class CodeGenerator(NodeVisitor): 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 @@ class CodeGenerator(NodeVisitor): 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 @@ class CodeGenerator(NodeVisitor): 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 @@ class CodeGenerator(NodeVisitor): 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 @@ string_re = re.compile(r"('([^'\\]*(?:\\.[^'\\]*)*)'" 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 @@ class Lexer(object): ] + 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 @@ class Lexer(object): value = float(value) elif token == 'operator': token = operators[value] - value = '' yield Token(lineno, token, value) return TokenStream(generate(), filename) @@ -398,12 +395,12 @@ class Lexer(object): 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 @@ class Lexer(object): # 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 @@ class Lexer(object): 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 @@ class Lexer(object): 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 Macro(Stmt): 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 @@ class Parser(object): 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 @@ class Parser(object): 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 StaticLoopContext(LoopContextBase): 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 @@ class Macro(object): 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 @@ 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: -- 2.26.2