Skip to content

Avoid errors in cleanup when event loop is already closed #1051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ Changelog

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


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 @@ -28,7 +34,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 @@ -1190,10 +1190,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)