]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Add support for explicit callers
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 8 Jan 2017 01:16:41 +0000 (02:16 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 8 Jan 2017 01:16:41 +0000 (02:16 +0100)
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
jinja2/compiler.py
jinja2/runtime.py
tests/test_regression.py

diff --git a/CHANGES b/CHANGES
index 2243be920b0b894c8483e37b20f34b9d232ea55d..dfa5a32dc19f13689cc72a149cbc262c416c5edb 100644 (file)
--- 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
 -------------
index 4f84a32aa732d8b969b3a8ddaf629886c6862ad5..9051ced6ed40f3b8ba6efd3a15c1d91725dd758c 100644 (file)
@@ -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
 
index 9a3c16ad72e27406d3d7f872fec78500d0618ff3..958ddfd426b91b96b88113a0ed4483261748754e 100644 (file)
@@ -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:
index 3230bfdd4e56497ba499f4ec00908602a2e99483..6f41e89a2bb06b79f355c61fdb37a16fa14b1607 100644 (file)
@@ -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')