Skip to content

Commit 1655128

Browse files
authored
test on trio, fix all missing aclose related warnings (#1960)
1 parent 079e831 commit 1655128

File tree

9 files changed

+238
-62
lines changed

9 files changed

+238
-62
lines changed

CHANGES.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ Unreleased
77

88
- Calling sync ``render`` for an async template uses ``asyncio.run``.
99
:pr:`1952`
10+
- Avoid unclosed ``auto_aiter`` warnings. :pr:`1960`
11+
- Return an ``aclose``-able ``AsyncGenerator`` from
12+
``Template.generate_async``. :pr:`1960`
13+
- Avoid leaving ``root_render_func()`` unclosed in
14+
``Template.generate_async``. :pr:`1960`
15+
- Avoid leaving async generators unclosed in blocks, includes and extends.
16+
:pr:`1960`
1017

1118

1219
Version 3.1.4

requirements/docs.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ charset-normalizer==3.1.0
1515
# via requests
1616
docutils==0.20.1
1717
# via sphinx
18-
idna==3.4
18+
idna==3.6
1919
# via requests
2020
imagesize==1.4.1
2121
# via sphinx

requirements/tests.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pytest
2+
trio<=0.22.2 # for Python3.7 support

requirements/tests.txt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1-
# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee
1+
# SHA1:b8d151f902b43c4435188a9d3494fb8d4af07168
22
#
33
# This file is autogenerated by pip-compile-multi
44
# To update, run:
55
#
66
# pip-compile-multi
77
#
8+
attrs==23.2.0
9+
# via
10+
# outcome
11+
# trio
812
exceptiongroup==1.1.1
9-
# via pytest
13+
# via
14+
# pytest
15+
# trio
16+
idna==3.6
17+
# via trio
1018
iniconfig==2.0.0
1119
# via pytest
20+
outcome==1.3.0.post0
21+
# via trio
1222
packaging==23.1
1323
# via pytest
1424
pluggy==1.2.0
1525
# via pytest
1626
pytest==7.4.0
1727
# via -r requirements/tests.in
28+
sniffio==1.3.1
29+
# via trio
30+
sortedcontainers==2.4.0
31+
# via trio
1832
tomli==2.0.1
1933
# via pytest
34+
trio==0.22.2
35+
# via -r requirements/tests.in

src/jinja2/async_utils.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from .utils import _PassArg
77
from .utils import pass_eval_context
88

9+
if t.TYPE_CHECKING:
10+
import typing_extensions as te
11+
912
V = t.TypeVar("V")
1013

1114

@@ -67,15 +70,27 @@ async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V":
6770
return t.cast("V", value)
6871

6972

70-
async def auto_aiter(
73+
class _IteratorToAsyncIterator(t.Generic[V]):
74+
def __init__(self, iterator: "t.Iterator[V]"):
75+
self._iterator = iterator
76+
77+
def __aiter__(self) -> "te.Self":
78+
return self
79+
80+
async def __anext__(self) -> V:
81+
try:
82+
return next(self._iterator)
83+
except StopIteration as e:
84+
raise StopAsyncIteration(e.value) from e
85+
86+
87+
def auto_aiter(
7188
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
7289
) -> "t.AsyncIterator[V]":
7390
if hasattr(iterable, "__aiter__"):
74-
async for item in t.cast("t.AsyncIterable[V]", iterable):
75-
yield item
91+
return iterable.__aiter__()
7692
else:
77-
for item in iterable:
78-
yield item
93+
return _IteratorToAsyncIterator(iter(iterable))
7994

8095

8196
async def auto_to_list(

src/jinja2/compiler.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -902,12 +902,15 @@ def visit_Template(
902902
if not self.environment.is_async:
903903
self.writeline("yield from parent_template.root_render_func(context)")
904904
else:
905-
self.writeline(
906-
"async for event in parent_template.root_render_func(context):"
907-
)
905+
self.writeline("agen = parent_template.root_render_func(context)")
906+
self.writeline("try:")
907+
self.indent()
908+
self.writeline("async for event in agen:")
908909
self.indent()
909910
self.writeline("yield event")
910911
self.outdent()
912+
self.outdent()
913+
self.writeline("finally: await agen.aclose()")
911914
self.outdent(1 + (not self.has_known_extends))
912915

913916
# at this point we now have the blocks collected and can visit them too.
@@ -977,14 +980,20 @@ def visit_Block(self, node: nodes.Block, frame: Frame) -> None:
977980
f"yield from context.blocks[{node.name!r}][0]({context})", node
978981
)
979982
else:
983+
self.writeline(f"gen = context.blocks[{node.name!r}][0]({context})")
984+
self.writeline("try:")
985+
self.indent()
980986
self.writeline(
981-
f"{self.choose_async()}for event in"
982-
f" context.blocks[{node.name!r}][0]({context}):",
987+
f"{self.choose_async()}for event in gen:",
983988
node,
984989
)
985990
self.indent()
986991
self.simple_write("event", frame)
987992
self.outdent()
993+
self.outdent()
994+
self.writeline(
995+
f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
996+
)
988997

989998
self.outdent(level)
990999

@@ -1057,26 +1066,33 @@ def visit_Include(self, node: nodes.Include, frame: Frame) -> None:
10571066
self.writeline("else:")
10581067
self.indent()
10591068

1060-
skip_event_yield = False
1069+
def loop_body() -> None:
1070+
self.indent()
1071+
self.simple_write("event", frame)
1072+
self.outdent()
1073+
10611074
if node.with_context:
10621075
self.writeline(
1063-
f"{self.choose_async()}for event in template.root_render_func("
1076+
f"gen = template.root_render_func("
10641077
"template.new_context(context.get_all(), True,"
1065-
f" {self.dump_local_context(frame)})):"
1078+
f" {self.dump_local_context(frame)}))"
1079+
)
1080+
self.writeline("try:")
1081+
self.indent()
1082+
self.writeline(f"{self.choose_async()}for event in gen:")
1083+
loop_body()
1084+
self.outdent()
1085+
self.writeline(
1086+
f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
10661087
)
10671088
elif self.environment.is_async:
10681089
self.writeline(
10691090
"for event in (await template._get_default_module_async())"
10701091
"._body_stream:"
10711092
)
1093+
loop_body()
10721094
else:
10731095
self.writeline("yield from template._get_default_module()._body_stream")
1074-
skip_event_yield = True
1075-
1076-
if not skip_event_yield:
1077-
self.indent()
1078-
self.simple_write("event", frame)
1079-
self.outdent()
10801096

10811097
if node.ignore_missing:
10821098
self.outdent()

src/jinja2/environment.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,7 +1346,7 @@ async def to_list() -> t.List[str]:
13461346

13471347
async def generate_async(
13481348
self, *args: t.Any, **kwargs: t.Any
1349-
) -> t.AsyncIterator[str]:
1349+
) -> t.AsyncGenerator[str, object]:
13501350
"""An async version of :meth:`generate`. Works very similarly but
13511351
returns an async iterator instead.
13521352
"""
@@ -1358,8 +1358,14 @@ async def generate_async(
13581358
ctx = self.new_context(dict(*args, **kwargs))
13591359

13601360
try:
1361-
async for event in self.root_render_func(ctx): # type: ignore
1362-
yield event
1361+
agen = self.root_render_func(ctx)
1362+
try:
1363+
async for event in agen: # type: ignore
1364+
yield event
1365+
finally:
1366+
# we can't use async with aclosing(...) because that's only
1367+
# in 3.10+
1368+
await agen.aclose() # type: ignore
13631369
except Exception:
13641370
yield self.environment.handle_exception()
13651371

0 commit comments

Comments
 (0)