summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArmin Ronacher <armin.ronacher@active-4.com>2017-01-08 02:19:12 +0100
committerArmin Ronacher <armin.ronacher@active-4.com>2017-01-08 02:19:12 +0100
commit7064298b53bdf04fff613f5237b4eaa3d24fa9c5 (patch)
tree352a75e3bbe63d47bc0ee8f3990791e3af146b05
parenta8df1f7392b60197d9859a0e09f2bea859769c30 (diff)
parent6235644d33ca6b378cc7b46e4f8ded16b4735714 (diff)
downloadjinja2-7064298b53bdf04fff613f5237b4eaa3d24fa9c5.tar.gz
Merge branch '2.9-maintenance'
-rw-r--r--CHANGES8
-rw-r--r--jinja2/compiler.py29
-rw-r--r--jinja2/runtime.py16
-rw-r--r--tests/test_regression.py22
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')