Skip to content

Commit 16f86bd

Browse files
committed
asyncio: #10 shutdown_asyncgens
Ensure any async generators that are left running past run_until_complete (essentially only if we get a KeyboardInterrupt that stops the run_until_complete) then those are finalized properly Note I don't use loop.shutdown_asyncgens sothat I can make the generator handle an asyncio.CancelledError rather than a GeneratorExit
1 parent db9d9cc commit 16f86bd

File tree

3 files changed

+108
-1
lines changed

3 files changed

+108
-1
lines changed

alt_pytest_asyncio/plugin.py

+35
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,46 @@ def __exit__(self, exc_typ, exc, tb):
201201
try:
202202
if getattr(self, "loop", None):
203203
cancel_all_tasks(self.loop, ignore_errors_from_tasks=self.tasks)
204+
self.loop.run_until_complete(self.shutdown_asyncgens())
204205
self.loop.close()
205206
finally:
206207
if hasattr(self, "_original_loop"):
207208
asyncio.set_event_loop(self._original_loop)
208209

210+
async def shutdown_asyncgens(self):
211+
"""
212+
A version of loop.shutdown_asyncgens that tries to cancel the generators
213+
before closing them.
214+
"""
215+
if not len(self.loop._asyncgens):
216+
return
217+
218+
closing_agens = list(self.loop._asyncgens)
219+
self.loop._asyncgens.clear()
220+
221+
# I would do an asyncio.tasks.gather but it would appear that just causes
222+
# the asyncio loop to think it's shutdown, so I have to do them one at a time
223+
for ag in closing_agens:
224+
try:
225+
try:
226+
try:
227+
await ag.athrow(asyncio.CancelledError())
228+
except StopAsyncIteration:
229+
pass
230+
finally:
231+
await ag.aclose()
232+
except asyncio.CancelledError:
233+
pass
234+
except:
235+
exc = sys.exc_info()[1]
236+
self.loop.call_exception_handler(
237+
{
238+
"message": "an error occurred during closing of asynchronous generator",
239+
"exception": exc,
240+
"asyncgen": ag,
241+
}
242+
)
243+
209244
def run_until_complete(self, coro):
210245
if not hasattr(self, "loop"):
211246
raise Exception(

pylama.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
skip = */setup.py
33

44
[pylama:tests/*]
5-
ignore = E225,E202,E211,E231,E226,W292,W291,E251,E122,E501,E701,E227,E305,E128,W391
5+
ignore = E225,E202,E211,E231,E226,W292,W291,E251,E122,E501,E701,E227,E305,E128,W391,C901
66

77
[pylama:pyflakes]
88
builtins = _

tests/test_override_loop.py

+72
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,78 @@ async def blah():
133133
asyncio.get_event_loop().run_until_complete(info["coro"])
134134

135135

136+
it "can shutdown async gens":
137+
info1 = []
138+
info2 = []
139+
info3 = []
140+
141+
original = asyncio.get_event_loop()
142+
143+
async def my_generator(info):
144+
try:
145+
info.append(1)
146+
yield
147+
info.append(2)
148+
yield
149+
info.append(3)
150+
except asyncio.CancelledError:
151+
info.append("cancelled")
152+
raise
153+
finally:
154+
info.append(("done", __import__("sys").exc_info()[0]))
155+
156+
# Test that the outside loop isn't affected by the inside loop
157+
outside_gen = my_generator(info1)
158+
159+
async def outside1():
160+
await outside_gen.__anext__()
161+
await outside_gen.__anext__()
162+
163+
original.run_until_complete(outside1())
164+
assert info1 == [1, 2]
165+
166+
# The way python asyncio works
167+
# Means that by defining this outside our OverrideLoop
168+
# The weakref held against it in the _asyncgens set on the loop
169+
# Will remain so that our shutdown_asyncgens function may work
170+
ag = my_generator(info2)
171+
172+
with OverrideLoop(new_loop=True) as custom_loop:
173+
assert info2 == []
174+
assert info3 == []
175+
176+
async def doit():
177+
ag2 = my_generator(info3)
178+
assert set(asyncio.get_event_loop()._asyncgens) == set()
179+
await ag2.__anext__()
180+
assert set(asyncio.get_event_loop()._asyncgens) == set([ag2])
181+
await ag.__anext__()
182+
assert set(asyncio.get_event_loop()._asyncgens) == set([ag2, ag])
183+
await ag.__anext__()
184+
assert info3 == [1]
185+
186+
custom_loop.run_until_complete(doit())
187+
assert list(custom_loop.loop._asyncgens) == [ag]
188+
assert info3 == [1]
189+
assert info2 == [1, 2]
190+
191+
assert asyncio.get_event_loop() is original
192+
assert not original.is_closed()
193+
194+
assert info3 == [1, "cancelled", ("done", asyncio.CancelledError)]
195+
assert info2 == [1, 2, "cancelled", ("done", asyncio.CancelledError)]
196+
assert info1 == [1, 2]
197+
198+
async def outside2():
199+
try:
200+
await outside_gen.__anext__()
201+
except StopAsyncIteration:
202+
pass
203+
204+
# Test that the outside loop isn't affected by the inside loop
205+
original.run_until_complete(outside2())
206+
assert info1 == [1, 2, 3, ("done", None)]
207+
136208
describe "testing autouse":
137209

138210
@pytest.fixture(autouse=True)

0 commit comments

Comments
 (0)