- Calling sync ``render`` for an async template uses ``asyncio.run``.
:pr:`1952`
+- Avoid unclosed ``auto_aiter`` warnings. :pr:`1960`
+- Return an ``aclose``-able ``AsyncGenerator`` from
+ ``Template.generate_async``. :pr:`1960`
+- Avoid leaving ``root_render_func()`` unclosed in
+ ``Template.generate_async``. :pr:`1960`
+- Avoid leaving async generators unclosed in blocks, includes and extends.
+ :pr:`1960`
Version 3.1.4
# via requests
docutils==0.20.1
# via sphinx
-idna==3.4
+idna==3.6
# via requests
imagesize==1.4.1
# via sphinx
pytest
+trio<=0.22.2 # for Python3.7 support
-# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee
+# SHA1:b8d151f902b43c4435188a9d3494fb8d4af07168
#
# This file is autogenerated by pip-compile-multi
# To update, run:
#
# pip-compile-multi
#
+attrs==23.2.0
+ # via
+ # outcome
+ # trio
exceptiongroup==1.1.1
- # via pytest
+ # via
+ # pytest
+ # trio
+idna==3.6
+ # via trio
iniconfig==2.0.0
# via pytest
+outcome==1.3.0.post0
+ # via trio
packaging==23.1
# via pytest
pluggy==1.2.0
# via pytest
pytest==7.4.0
# via -r requirements/tests.in
+sniffio==1.3.1
+ # via trio
+sortedcontainers==2.4.0
+ # via trio
tomli==2.0.1
# via pytest
+trio==0.22.2
+ # via -r requirements/tests.in
from .utils import _PassArg
from .utils import pass_eval_context
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
V = t.TypeVar("V")
return t.cast("V", value)
-async def auto_aiter(
+class _IteratorToAsyncIterator(t.Generic[V]):
+ def __init__(self, iterator: "t.Iterator[V]"):
+ self._iterator = iterator
+
+ def __aiter__(self) -> "te.Self":
+ return self
+
+ async def __anext__(self) -> V:
+ try:
+ return next(self._iterator)
+ except StopIteration as e:
+ raise StopAsyncIteration(e.value) from e
+
+
+def auto_aiter(
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
) -> "t.AsyncIterator[V]":
if hasattr(iterable, "__aiter__"):
- async for item in t.cast("t.AsyncIterable[V]", iterable):
- yield item
+ return iterable.__aiter__()
else:
- for item in iterable:
- yield item
+ return _IteratorToAsyncIterator(iter(iterable))
async def auto_to_list(
if not self.environment.is_async:
self.writeline("yield from parent_template.root_render_func(context)")
else:
- self.writeline(
- "async for event in parent_template.root_render_func(context):"
- )
+ self.writeline("agen = parent_template.root_render_func(context)")
+ self.writeline("try:")
+ self.indent()
+ self.writeline("async for event in agen:")
self.indent()
self.writeline("yield event")
self.outdent()
+ self.outdent()
+ self.writeline("finally: await agen.aclose()")
self.outdent(1 + (not self.has_known_extends))
# at this point we now have the blocks collected and can visit them too.
f"yield from context.blocks[{node.name!r}][0]({context})", node
)
else:
+ self.writeline(f"gen = context.blocks[{node.name!r}][0]({context})")
+ self.writeline("try:")
+ self.indent()
self.writeline(
- f"{self.choose_async()}for event in"
- f" context.blocks[{node.name!r}][0]({context}):",
+ f"{self.choose_async()}for event in gen:",
node,
)
self.indent()
self.simple_write("event", frame)
self.outdent()
+ self.outdent()
+ self.writeline(
+ f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
+ )
self.outdent(level)
self.writeline("else:")
self.indent()
- skip_event_yield = False
+ def loop_body() -> None:
+ self.indent()
+ self.simple_write("event", frame)
+ self.outdent()
+
if node.with_context:
self.writeline(
- f"{self.choose_async()}for event in template.root_render_func("
+ f"gen = template.root_render_func("
"template.new_context(context.get_all(), True,"
- f" {self.dump_local_context(frame)})):"
+ f" {self.dump_local_context(frame)}))"
+ )
+ self.writeline("try:")
+ self.indent()
+ self.writeline(f"{self.choose_async()}for event in gen:")
+ loop_body()
+ self.outdent()
+ self.writeline(
+ f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
)
elif self.environment.is_async:
self.writeline(
"for event in (await template._get_default_module_async())"
"._body_stream:"
)
+ loop_body()
else:
self.writeline("yield from template._get_default_module()._body_stream")
- skip_event_yield = True
-
- if not skip_event_yield:
- self.indent()
- self.simple_write("event", frame)
- self.outdent()
if node.ignore_missing:
self.outdent()
async def generate_async(
self, *args: t.Any, **kwargs: t.Any
- ) -> t.AsyncIterator[str]:
+ ) -> t.AsyncGenerator[str, object]:
"""An async version of :meth:`generate`. Works very similarly but
returns an async iterator instead.
"""
ctx = self.new_context(dict(*args, **kwargs))
try:
- async for event in self.root_render_func(ctx): # type: ignore
- yield event
+ agen = self.root_render_func(ctx)
+ try:
+ async for event in agen: # type: ignore
+ yield event
+ finally:
+ # we can't use async with aclosing(...) because that's only
+ # in 3.10+
+ await agen.aclose() # type: ignore
except Exception:
yield self.environment.handle_exception()
import asyncio
import pytest
+import trio
from jinja2 import ChainableUndefined
from jinja2 import DictLoader
from jinja2.nativetypes import NativeEnvironment
-def test_basic_async():
+def _asyncio_run(async_fn, *args):
+ return asyncio.run(async_fn(*args))
+
+
+@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"])
+def run_async_fn(request):
+ return request.param
+
+
+def test_basic_async(run_async_fn):
t = Template(
"{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
)
async def func():
return await t.render_async()
- rv = asyncio.run(func())
+ rv = run_async_fn(func)
assert rv == "[1][2][3]"
-def test_await_on_calls():
+def test_await_on_calls(run_async_fn):
t = Template("{{ async_func() + normal_func() }}", enable_async=True)
async def async_func():
async def func():
return await t.render_async(async_func=async_func, normal_func=normal_func)
- rv = asyncio.run(func())
+ rv = run_async_fn(func)
assert rv == "65"
assert rv == "65"
-def test_await_and_macros():
+def test_await_and_macros(run_async_fn):
t = Template(
"{% macro foo(x) %}[{{ x }}][{{ async_func() }}]{% endmacro %}{{ foo(42) }}",
enable_async=True,
async def func():
return await t.render_async(async_func=async_func)
- rv = asyncio.run(func())
+ rv = run_async_fn(func)
assert rv == "[42][42]"
-def test_async_blocks():
+def test_async_blocks(run_async_fn):
t = Template(
"{% block foo %}<Test>{% endblock %}{{ self.foo() }}",
enable_async=True,
async def func():
return await t.render_async()
- rv = asyncio.run(func())
+ rv = run_async_fn(func)
assert rv == "<Test><Test>"
test_env_async.from_string('{% from "foo" import bar, with, context %}')
test_env_async.from_string('{% from "foo" import bar, with with context %}')
- def test_exports(self, test_env_async):
- coro = test_env_async.from_string(
+ def test_exports(self, test_env_async, run_async_fn):
+ coro_fn = test_env_async.from_string(
"""
{% macro toplevel() %}...{% endmacro %}
{% macro __private() %}...{% endmacro %}
{% macro notthere() %}{% endmacro %}
{% endfor %}
"""
- )._get_default_module_async()
- m = asyncio.run(coro)
- assert asyncio.run(m.toplevel()) == "..."
+ )._get_default_module_async
+ m = run_async_fn(coro_fn)
+ assert run_async_fn(m.toplevel) == "..."
assert not hasattr(m, "__missing")
assert m.variable == 42
assert not hasattr(m, "notthere")
)
assert tmpl.render(items=reversed([3, 2, 1])) == "1,2,3"
- def test_loop_errors(self, test_env_async):
+ def test_loop_errors(self, test_env_async, run_async_fn):
tmpl = test_env_async.from_string(
"""{% for item in [1] if loop.index
== 0 %}...{% endfor %}"""
)
- pytest.raises(UndefinedError, tmpl.render)
+ with pytest.raises(UndefinedError):
+ run_async_fn(tmpl.render_async)
+
tmpl = test_env_async.from_string(
"""{% for item in [] %}...{% else
%}{{ loop }}{% endfor %}"""
)
- assert tmpl.render() == ""
+ assert run_async_fn(tmpl.render_async) == ""
def test_loop_filter(self, test_env_async):
tmpl = test_env_async.from_string(
assert t.render(a=dict(b=[1, 2, 3])) == "1"
-def test_namespace_awaitable(test_env_async):
+def test_namespace_awaitable(test_env_async, run_async_fn):
async def _test():
t = test_env_async.from_string(
'{% set ns = namespace(foo="Bar") %}{{ ns.foo }}'
actual = await t.render_async()
assert actual == "Bar"
- asyncio.run(_test())
+ run_async_fn(_test)
-def test_chainable_undefined_aiter():
+def test_chainable_undefined_aiter(run_async_fn):
async def _test():
t = Template(
"{% for x in a['b']['c'] %}{{ x }}{% endfor %}",
rv = await t.render_async(a={})
assert rv == ""
- asyncio.run(_test())
+ run_async_fn(_test)
@pytest.fixture
return NativeEnvironment(enable_async=True)
-def test_native_async(async_native_env):
+def test_native_async(async_native_env, run_async_fn):
async def _test():
t = async_native_env.from_string("{{ x }}")
rv = await t.render_async(x=23)
assert rv == 23
- asyncio.run(_test())
+ run_async_fn(_test)
-def test_native_list_async(async_native_env):
+def test_native_list_async(async_native_env, run_async_fn):
async def _test():
t = async_native_env.from_string("{{ x }}")
rv = await t.render_async(x=list(range(3)))
assert rv == [0, 1, 2]
- asyncio.run(_test())
+ run_async_fn(_test)
def test_getitem_after_filter():
t = env.from_string("{{ add_each(a, 2)[1:] }}")
out = t.render(a=range(3))
assert out == "[3, 4]"
+
+
+def test_basic_generate_async(run_async_fn):
+ t = Template(
+ "{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
+ )
+
+ async def func():
+ agen = t.generate_async()
+ try:
+ return await agen.__anext__()
+ finally:
+ await agen.aclose()
+
+ rv = run_async_fn(func)
+ assert rv == "["
+
+
+def test_include_generate_async(run_async_fn, test_env_async):
+ t = test_env_async.from_string('{% include "header" %}')
+
+ async def func():
+ agen = t.generate_async()
+ try:
+ return await agen.__anext__()
+ finally:
+ await agen.aclose()
+
+ rv = run_async_fn(func)
+ assert rv == "["
+
+
+def test_blocks_generate_async(run_async_fn):
+ t = Template(
+ "{% block foo %}<Test>{% endblock %}{{ self.foo() }}",
+ enable_async=True,
+ autoescape=True,
+ )
+
+ async def func():
+ agen = t.generate_async()
+ try:
+ return await agen.__anext__()
+ finally:
+ await agen.aclose()
+
+ rv = run_async_fn(func)
+ assert rv == "<Test>"
+
+
+def test_async_extend(run_async_fn, test_env_async):
+ t = test_env_async.from_string('{% extends "header" %}')
+
+ async def func():
+ agen = t.generate_async()
+ try:
+ return await agen.__anext__()
+ finally:
+ await agen.aclose()
+
+ rv = run_async_fn(func)
+ assert rv == "["
+import asyncio
+import contextlib
from collections import namedtuple
import pytest
+import trio
from markupsafe import Markup
from jinja2 import Environment
return Environment(enable_async=True)
+def _asyncio_run(async_fn, *args):
+ return asyncio.run(async_fn(*args))
+
+
+@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"])
+def run_async_fn(request):
+ return request.param
+
+
+@contextlib.asynccontextmanager
+async def closing_factory():
+ async with contextlib.AsyncExitStack() as stack:
+
+ def closing(maybe_agen):
+ try:
+ aclose = maybe_agen.aclose
+ except AttributeError:
+ pass
+ else:
+ stack.push_async_callback(aclose)
+ return maybe_agen
+
+ yield closing
+
+
@mark_dualiter("foo", lambda: range(10))
-def test_first(env_async, foo):
- tmpl = env_async.from_string("{{ foo()|first }}")
- out = tmpl.render(foo=foo)
+def test_first(env_async, foo, run_async_fn):
+ async def test():
+ async with closing_factory() as closing:
+ tmpl = env_async.from_string("{{ closing(foo())|first }}")
+ return await tmpl.render_async(foo=foo, closing=closing)
+
+ out = run_async_fn(test)
assert out == "0"
)
-def test_custom_async_filter(env_async):
+def test_custom_async_filter(env_async, run_async_fn):
async def customfilter(val):
return str(val)
- env_async.filters["customfilter"] = customfilter
- tmpl = env_async.from_string("{{ 'static'|customfilter }} {{ arg|customfilter }}")
- out = tmpl.render(arg="dynamic")
+ async def test():
+ env_async.filters["customfilter"] = customfilter
+ tmpl = env_async.from_string(
+ "{{ 'static'|customfilter }} {{ arg|customfilter }}"
+ )
+ return await tmpl.render_async(arg="dynamic")
+
+ out = run_async_fn(test)
assert out == "static dynamic"
@mark_dualiter("items", lambda: range(10))
-def test_custom_async_iteratable_filter(env_async, items):
+def test_custom_async_iteratable_filter(env_async, items, run_async_fn):
async def customfilter(iterable):
items = []
async for item in auto_aiter(iterable):
break
return ",".join(items)
- env_async.filters["customfilter"] = customfilter
- tmpl = env_async.from_string(
- "{{ items()|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}"
- )
- out = tmpl.render(items=items)
+ async def test():
+ async with closing_factory() as closing:
+ env_async.filters["customfilter"] = customfilter
+ tmpl = env_async.from_string(
+ "{{ closing(items())|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}"
+ )
+ return await tmpl.render_async(items=items, closing=closing)
+
+ out = run_async_fn(test)
assert out == "0,1,2 .. 3,4,5"