Skip to content

Commit 7c50192

Browse files
minrkseifertm
authored andcommittedJan 28, 2025
fix: Avoid errors in cleanup of async generators when event loop is already closed
check `is_closed()` before calling cleanup methods and degrade exceptions to warnings during cleanup to avoid problems
1 parent 2188cdb commit 7c50192

File tree

3 files changed

+72
-5
lines changed

3 files changed

+72
-5
lines changed
 

‎docs/reference/changelog.rst

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22
Changelog
33
=========
44

5+
0.25.3 (2025-01-28)
6+
===================
7+
- Avoid errors in cleanup of async generators when event loop is already closed `#1040 <https://github.com/pytest-dev/pytest-asyncio/issues/1040>`_
8+
9+
510
0.25.2 (2025-01-08)
611
===================
712

813
- Call ``loop.shutdown_asyncgens()`` before closing the event loop to ensure async generators are closed in the same manner as ``asyncio.run`` does `#1034 <https://github.com/pytest-dev/pytest-asyncio/pull/1034>`_
914

15+
1016
0.25.1 (2025-01-02)
1117
===================
1218
- Fixes an issue that caused a broken event loop when a function-scoped test was executed in between two tests with wider loop scope `#950 <https://github.com/pytest-dev/pytest-asyncio/issues/950>`_
@@ -22,7 +28,6 @@ Changelog
2228
- Propagates `contextvars` set in async fixtures to other fixtures and tests on Python 3.11 and above. `#1008 <https://github.com/pytest-dev/pytest-asyncio/pull/1008>`_
2329

2430

25-
2631
0.24.0 (2024-08-22)
2732
===================
2833
- BREAKING: Updated minimum supported pytest version to v8.2.0

‎pytest_asyncio/plugin.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -1164,10 +1164,14 @@ def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]:
11641164
try:
11651165
yield loop
11661166
finally:
1167-
try:
1168-
loop.run_until_complete(loop.shutdown_asyncgens())
1169-
finally:
1170-
loop.close()
1167+
# cleanup the event loop if it hasn't been cleaned up already
1168+
if not loop.is_closed():
1169+
try:
1170+
loop.run_until_complete(loop.shutdown_asyncgens())
1171+
except Exception as e:
1172+
warnings.warn(f"Error cleaning up asyncio loop: {e}", RuntimeWarning)
1173+
finally:
1174+
loop.close()
11711175

11721176

11731177
@pytest.fixture(scope="session")

‎tests/test_event_loop_fixture.py

+58
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,61 @@ async def generator_fn():
8080
)
8181
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
8282
result.assert_outcomes(passed=1, warnings=0)
83+
84+
85+
def test_event_loop_already_closed(
86+
pytester: Pytester,
87+
):
88+
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
89+
pytester.makepyfile(
90+
dedent(
91+
"""\
92+
import asyncio
93+
import pytest
94+
import pytest_asyncio
95+
pytest_plugins = 'pytest_asyncio'
96+
97+
@pytest_asyncio.fixture
98+
async def _event_loop():
99+
return asyncio.get_running_loop()
100+
101+
@pytest.fixture
102+
def cleanup_after(_event_loop):
103+
yield
104+
# fixture has its own cleanup code
105+
_event_loop.close()
106+
107+
@pytest.mark.asyncio
108+
async def test_something(cleanup_after):
109+
await asyncio.sleep(0.01)
110+
"""
111+
)
112+
)
113+
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
114+
result.assert_outcomes(passed=1, warnings=0)
115+
116+
117+
def test_event_loop_fixture_asyncgen_error(
118+
pytester: Pytester,
119+
):
120+
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
121+
pytester.makepyfile(
122+
dedent(
123+
"""\
124+
import asyncio
125+
import pytest
126+
127+
pytest_plugins = 'pytest_asyncio'
128+
129+
@pytest.mark.asyncio
130+
async def test_something():
131+
# mock shutdown_asyncgen failure
132+
loop = asyncio.get_running_loop()
133+
async def fail():
134+
raise RuntimeError("mock error cleaning up...")
135+
loop.shutdown_asyncgens = fail
136+
"""
137+
)
138+
)
139+
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
140+
result.assert_outcomes(passed=1, warnings=1)

0 commit comments

Comments
 (0)