From 6235644d33ca6b378cc7b46e4f8ded16b4735714 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 8 Jan 2017 02:16:41 +0100 Subject: Add support for explicit callers This adds support for a never intended Jinja2 feature which however worked in limited support before due to a bug with the identifier scoping. A quick github code search indicates that developers commonly did this to set the default caller to none. This fixes #642 --- CHANGES | 8 +++++++- jinja2/compiler.py | 29 +++++++++++++++++++++++++---- jinja2/runtime.py | 16 +++++++++++++++- tests/test_regression.py | 22 +++++++++++++++++++++- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index 2243be9..dfa5a32 100644 --- a/CHANGES +++ b/CHANGES @@ -1,12 +1,18 @@ Jinja2 Changelog ================ -Version 2.9.1 +Version 2.9.2 ------------- (bugfix release, release date undecided) - Fixed a regression that caused for loops to not be able to use the same variable for the target as well as source iterator. (#640) +- Add support for a previously unknown behavior of macros. It used to be + possible in some circumstances to explicitly provide a caller argument + to macros. While badly buggy and unintended it turns out that this is a + common case that gets copy pasted around. To not completely break backwards + compatibility with the most common cases it's now possible to provide an + explicit keyword argument for caller if it's given an explicit default. Version 2.9.1 ------------- diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 4f84a32..9051ced 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -503,18 +503,39 @@ class CodeGenerator(NodeVisitor): frame.symbols.analyze_node(node) macro_ref = MacroRef(node) + explicit_caller = None + skip_special_params = set() args = [] - for arg in node.args: + for idx, arg in enumerate(node.args): + if arg.name == 'caller': + explicit_caller = idx + if arg.name in ('kwargs', 'varargs'): + skip_special_params.add(arg.name) args.append(frame.symbols.ref(arg.name)) undeclared = find_undeclared(node.body, ('caller', 'kwargs', 'varargs')) + if 'caller' in undeclared: - args.append(frame.symbols.declare_parameter('caller')) + # In older Jinja2 versions there was a bug that allowed caller + # to retain the special behavior even if it was mentioned in + # the argument list. However thankfully this was only really + # working if it was the last argument. So we are explicitly + # checking this now and error out if it is anywhere else in + # the argument list. + if explicit_caller is not None: + try: + node.defaults[explicit_caller - len(node.args)] + except IndexError: + self.fail('When defining macros or call blocks the ' + 'special "caller" argument must be omitted ' + 'or be given a default.', node.lineno) + else: + args.append(frame.symbols.declare_parameter('caller')) macro_ref.accesses_caller = True - if 'kwargs' in undeclared: + if 'kwargs' in undeclared and not 'kwargs' in skip_special_params: args.append(frame.symbols.declare_parameter('kwargs')) macro_ref.accesses_kwargs = True - if 'varargs' in undeclared: + if 'varargs' in undeclared and not 'varargs' in skip_special_params: args.append(frame.symbols.declare_parameter('varargs')) macro_ref.accesses_varargs = True diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 9a3c16a..958ddfd 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -415,6 +415,7 @@ class Macro(object): self.catch_kwargs = catch_kwargs self.catch_varargs = catch_varargs self.caller = caller + self.explicit_caller = 'caller' in arguments if default_autoescape is None: default_autoescape = environment.autoescape self._default_autoescape = default_autoescape @@ -449,6 +450,10 @@ class Macro(object): arguments = list(args[:self._argument_count]) off = len(arguments) + # For information why this is necessary refer to the handling + # of caller in the `macro_body` handler in the compiler. + found_caller = False + # if the number of arguments consumed is not the number of # arguments expected we start filling in keyword arguments # and defaults. @@ -458,20 +463,29 @@ class Macro(object): value = kwargs.pop(name) except KeyError: value = missing + if name == 'caller': + found_caller = True arguments.append(value) + else: + found_caller = self.explicit_caller # it's important that the order of these arguments does not change # if not also changed in the compiler's `function_scoping` method. # the order is caller, keyword arguments, positional arguments! - if self.caller: + if self.caller and not found_caller: caller = kwargs.pop('caller', None) if caller is None: caller = self._environment.undefined('No caller defined', name='caller') arguments.append(caller) + if self.catch_kwargs: arguments.append(kwargs) elif kwargs: + if 'caller' in kwargs: + raise TypeError('macro %r was invoked with two values for ' + 'the special caller argument. This is ' + 'most likely a bug.' % self.name) raise TypeError('macro %r takes no keyword argument %r' % (self.name, next(iter(kwargs)))) if self.catch_varargs: diff --git a/tests/test_regression.py b/tests/test_regression.py index 3230bfd..6f41e89 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -12,7 +12,7 @@ import sys import pytest from jinja2 import Template, Environment, DictLoader, TemplateSyntaxError, \ - TemplateNotFound, PrefixLoader + TemplateAssertionError, TemplateNotFound, PrefixLoader from jinja2._compat import text_type @@ -422,3 +422,23 @@ class TestBug(object): t = env.from_string('{% for x in x.y recursive %}{{ x }}{% endfor %}') assert t.render(x={'y': [0, 1, 2]}) == '012' + + def test_double_caller(self, env): + t = env.from_string('{% macro x(caller=none) %}[{% if caller %}' + '{{ caller() }}{% endif %}]{% endmacro %}' + '{{ x() }}{% call x() %}aha!{% endcall %}') + assert t.render() == '[][aha!]' + + def test_double_caller_no_default(self, env): + with pytest.raises(TemplateAssertionError) as exc_info: + env.from_string('{% macro x(caller) %}[{% if caller %}' + '{{ caller() }}{% endif %}]{% endmacro %}') + assert exc_info.match(r'"caller" argument must be omitted or ' + r'be given a default') + + t = env.from_string('{% macro x(caller=none) %}[{% if caller %}' + '{{ caller() }}{% endif %}]{% endmacro %}') + with pytest.raises(TypeError) as exc_info: + t.module.x(None, caller=lambda: 42) + assert exc_info.match(r'\'x\' was invoked with two values for the ' + r'special caller argument') -- cgit v1.2.1