]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
async templates await attribute access 1101/head
authorDavid Lord <davidism@gmail.com>
Fri, 8 Nov 2019 02:57:21 +0000 (18:57 -0800)
committerDavid Lord <davidism@gmail.com>
Fri, 8 Nov 2019 03:05:16 +0000 (19:05 -0800)
CHANGES.rst
jinja2/compiler.py
tests/test_async.py
tests/test_runtime.py

index f05603f58de89364a6de992cafe00b26cc7d56bf..07528541e801910e52327ca0857ad8671590ccf0 100644 (file)
@@ -5,11 +5,7 @@ Version 2.11.0
 
 Unreleased
 
--   Async support is only loaded the first time an
-    :class:`~environment.Environment` enables it, in order to avoid a
-    slow initial import. :issue:`765`
--   Python 2.6 and 3.3 are not supported anymore.
--   The ``map`` filter in async mode now automatically awaits
+-   Python 2.6, 3.3, and 3.4 are not supported anymore.
 -   Added a new ``ChainableUndefined`` class to support getitem and
     getattr on an undefined object. :issue:`977`
 -   Allow ``{%+`` syntax (with NOP behavior) when ``lstrip_blocks`` is
@@ -47,6 +43,18 @@ Unreleased
 -   Fix behavior of ``loop`` control variables such as ``length`` and
     ``revindex0`` when looping over a generator. :issue:`459, 751, 794`,
     :pr:`993`
+-   Async support is only loaded the first time an environment enables
+    it, in order to avoid a slow initial import. :issue:`765`
+-   In async environments, the ``|map`` filter will await the filter
+    call if needed. :pr:`913`
+-   In for loops that access ``loop`` attributes, the iterator is not
+    advanced ahead of the current iteration unless ``length``,
+    ``revindex``, ``nextitem``, or ``last`` are accessed. This makes it
+    less likely to break ``groupby`` results. :issue:`555`, :pr:`1101`
+-   In async environments, the ``loop`` attributes ``length`` and
+    ``revindex`` work for async iterators. :pr:`1101`
+-   In async environments, values from attribute/property access will
+    be awaited if needed. :pr:`1101`
 -   ``PackageLoader`` doesn't depend on setuptools or pkg_resources.
     :issue:`970`
 -   Support :class:`os.PathLike` objects in
index 00b29b8ef18e3527ca50365471db8d04437c859d..50e00ab267a9ad64ecce64f6b2367185436beaf3 100644 (file)
@@ -1551,10 +1551,16 @@ class CodeGenerator(NodeVisitor):
 
     @optimizeconst
     def visit_Getattr(self, node, frame):
+        if self.environment.is_async:
+            self.write("await auto_await(")
+
         self.write('environment.getattr(')
         self.visit(node.node, frame)
         self.write(', %r)' % node.attr)
 
+        if self.environment.is_async:
+            self.write(")")
+
     @optimizeconst
     def visit_Getitem(self, node, frame):
         # slices bypass the environment getitem method.
@@ -1564,12 +1570,18 @@ class CodeGenerator(NodeVisitor):
             self.visit(node.arg, frame)
             self.write(']')
         else:
+            if self.environment.is_async:
+                self.write("await auto_await(")
+
             self.write('environment.getitem(')
             self.visit(node.node, frame)
             self.write(', ')
             self.visit(node.arg, frame)
             self.write(')')
 
+            if self.environment.is_async:
+                self.write(")")
+
     def visit_Slice(self, node, frame):
         if node.start is not None:
             self.visit(node.start, frame)
index 92ac2a393b7b92f57ac6d213ffdfcb87a7528d31..5f331a51fbcc9066a32e7d5e80c4700eaf6448a5 100644 (file)
@@ -2,6 +2,7 @@ import pytest
 import asyncio
 
 from jinja2 import Template, Environment, DictLoader
+from jinja2.asyncsupport import auto_aiter
 from jinja2.exceptions import TemplateNotFound, TemplatesNotFound, \
      UndefinedError
 
@@ -274,26 +275,17 @@ class TestAsyncForLoop(object):
         tmpl = test_env_async.from_string('<{% for item in seq %}{% else %}{% endfor %}>')
         assert tmpl.render() == '<>'
 
-    def test_context_vars(self, test_env_async):
-        slist = [42, 24]
-        for seq in [slist, iter(slist), reversed(slist), (_ for _ in slist)]:
-            tmpl = test_env_async.from_string('''{% for item in seq -%}
-            {{ loop.index }}|{{ loop.index0 }}|{{ loop.revindex }}|{{
-                loop.revindex0 }}|{{ loop.first }}|{{ loop.last }}|{{
-               loop.length }}###{% endfor %}''')
-            one, two, _ = tmpl.render(seq=seq).split('###')
-            (one_index, one_index0, one_revindex, one_revindex0, one_first,
-             one_last, one_length) = one.split('|')
-            (two_index, two_index0, two_revindex, two_revindex0, two_first,
-             two_last, two_length) = two.split('|')
-
-            assert int(one_index) == 1 and int(two_index) == 2
-            assert int(one_index0) == 0 and int(two_index0) == 1
-            assert int(one_revindex) == 2 and int(two_revindex) == 1
-            assert int(one_revindex0) == 1 and int(two_revindex0) == 0
-            assert one_first == 'True' and two_first == 'False'
-            assert one_last == 'False' and two_last == 'True'
-            assert one_length == two_length == '2'
+    @pytest.mark.parametrize(
+        "transform", [lambda x: x, iter, reversed, lambda x: (i for i in x), auto_aiter]
+    )
+    def test_context_vars(self, test_env_async, transform):
+        t = test_env_async.from_string(
+            "{% for item in seq %}{{ loop.index }}|{{ loop.index0 }}"
+            "|{{ loop.revindex }}|{{ loop.revindex0 }}|{{ loop.first }}"
+            "|{{ loop.last }}|{{ loop.length }}\n{% endfor %}"
+        )
+        out = t.render(seq=transform([42, 24]))
+        assert out == "1|0|2|1|True|False|2\n2|1|1|0|False|True|2\n"
 
     def test_cycling(self, test_env_async):
         tmpl = test_env_async.from_string('''{% for item in seq %}{{
index 1b24b40dee24f80648354c5f71bdfbb6df85c197..1afcb3fad5ef1ebc002bbce0eb569d92e41d1b91 100644 (file)
@@ -1,3 +1,5 @@
+import itertools
+
 from jinja2 import Template
 from jinja2.runtime import LoopContext
 
@@ -46,3 +48,13 @@ def test_loopcontext2():
     in_lst = [10, 11]
     l = LoopContext(reversed(in_lst), None)
     assert l.length == len(in_lst)
+
+
+def test_iterator_not_advanced_early():
+    t = Template("{% for _, g in gs %}{{ loop.index }} {{ g|list }}\n{% endfor %}")
+    out = t.render(gs=itertools.groupby(
+        [(1, "a"), (1, "b"), (2, "c"), (3, "d")], lambda x: x[0]
+    ))
+    # groupby groups depend on the current position of the iterator. If
+    # it was advanced early, the lists would appear empty.
+    assert out == "1 [(1, 'a'), (1, 'b')]\n2 [(2, 'c')]\n3 [(3, 'd')]\n"