From 4d0949b3087e10c5bd183e7b7f22b15a74b95f68 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 7 Nov 2019 18:57:21 -0800 Subject: async templates await attribute access --- CHANGES.rst | 18 +++++++++++++----- jinja2/compiler.py | 12 ++++++++++++ tests/test_async.py | 32 ++++++++++++-------------------- tests/test_runtime.py | 12 ++++++++++++ 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f05603f..0752854 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 00b29b8..50e00ab 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -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) diff --git a/tests/test_async.py b/tests/test_async.py index 92ac2a3..5f331a5 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -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 %}{{ diff --git a/tests/test_runtime.py b/tests/test_runtime.py index 1b24b40..1afcb3f 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -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" -- cgit v1.2.1