Skip to content

Commit

Permalink
fix: Avoid errors in cleanup of async generators when event loop is a…
Browse files Browse the repository at this point in the history
…lready closed

check `is_closed()` before calling cleanup methods
and degrade exceptions to warnings during cleanup to avoid problems
  • Loading branch information
minrk authored and seifertm committed Jan 28, 2025
1 parent 2188cdb commit 7c50192
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 5 deletions.
7 changes: 6 additions & 1 deletion docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@
Changelog
=========

0.25.3 (2025-01-28)
===================
- Avoid errors in cleanup of async generators when event loop is already closed `#1040 <https://github.com/pytest-dev/pytest-asyncio/issues/1040>`_


0.25.2 (2025-01-08)
===================

- 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>`_


0.25.1 (2025-01-02)
===================
- 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>`_
Expand All @@ -22,7 +28,6 @@ Changelog
- 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>`_



0.24.0 (2024-08-22)
===================
- BREAKING: Updated minimum supported pytest version to v8.2.0
Expand Down
12 changes: 8 additions & 4 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,10 +1164,14 @@ def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]:
try:
yield loop
finally:
try:
loop.run_until_complete(loop.shutdown_asyncgens())
finally:
loop.close()
# cleanup the event loop if it hasn't been cleaned up already
if not loop.is_closed():
try:
loop.run_until_complete(loop.shutdown_asyncgens())
except Exception as e:
warnings.warn(f"Error cleaning up asyncio loop: {e}", RuntimeWarning)
finally:
loop.close()


@pytest.fixture(scope="session")
Expand Down
58 changes: 58 additions & 0 deletions tests/test_event_loop_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,61 @@ async def generator_fn():
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
result.assert_outcomes(passed=1, warnings=0)


def test_event_loop_already_closed(
pytester: Pytester,
):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
pytest_plugins = 'pytest_asyncio'
@pytest_asyncio.fixture
async def _event_loop():
return asyncio.get_running_loop()
@pytest.fixture
def cleanup_after(_event_loop):
yield
# fixture has its own cleanup code
_event_loop.close()
@pytest.mark.asyncio
async def test_something(cleanup_after):
await asyncio.sleep(0.01)
"""
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
result.assert_outcomes(passed=1, warnings=0)


def test_event_loop_fixture_asyncgen_error(
pytester: Pytester,
):
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
pytest_plugins = 'pytest_asyncio'
@pytest.mark.asyncio
async def test_something():
# mock shutdown_asyncgen failure
loop = asyncio.get_running_loop()
async def fail():
raise RuntimeError("mock error cleaning up...")
loop.shutdown_asyncgens = fail
"""
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default")
result.assert_outcomes(passed=1, warnings=1)

0 comments on commit 7c50192

Please sign in to comment.