Skip to content

Commit 7dafeb2

Browse files
minrkseifertm
authored andcommitted
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 92b006f commit 7dafeb2

File tree

3 files changed

+73
-6
lines changed

3 files changed

+73
-6
lines changed

docs/reference/changelog.rst

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@ Changelog
44

55
0.26.0 (UNRELEASED)
66
===================
7-
- Adds configuration option that sets default event loop scope for all testss `#793 <https://github.com/pytest-dev/pytest-asyncio/issues/793>`_
7+
- Adds configuration option that sets default event loop scope for all tests `#793 <https://github.com/pytest-dev/pytest-asyncio/issues/793>`_
88
- Improved type annotations for ``pytest_asyncio.fixture`` `#1045 <https://github.com/pytest-dev/pytest-asyncio/pull/1045>`_
99
- Added ``typing-extensions`` as additional dependency for Python ``<3.10`` `#1045 <https://github.com/pytest-dev/pytest-asyncio/pull/1045>`_
1010

1111

12+
0.25.3 (2025-01-28)
13+
===================
14+
- Avoid errors in cleanup of async generators when event loop is already closed `#1040 <https://github.com/pytest-dev/pytest-asyncio/issues/1040>`_
15+
16+
1217
0.25.2 (2025-01-08)
1318
===================
1419
- 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>`_
1520

21+
1622
0.25.1 (2025-01-02)
1723
===================
1824
- 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>`_
@@ -28,7 +34,6 @@ Changelog
2834
- 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>`_
2935

3036

31-
3237
0.24.0 (2024-08-22)
3338
===================
3439
- BREAKING: Updated minimum supported pytest version to v8.2.0

pytest_asyncio/plugin.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -1190,10 +1190,14 @@ def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]:
11901190
try:
11911191
yield loop
11921192
finally:
1193-
try:
1194-
loop.run_until_complete(loop.shutdown_asyncgens())
1195-
finally:
1196-
loop.close()
1193+
# cleanup the event loop if it hasn't been cleaned up already
1194+
if not loop.is_closed():
1195+
try:
1196+
loop.run_until_complete(loop.shutdown_asyncgens())
1197+
except Exception as e:
1198+
warnings.warn(f"Error cleaning up asyncio loop: {e}", RuntimeWarning)
1199+
finally:
1200+
loop.close()
11971201

11981202

11991203
@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)