diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c79734ca..c1943f60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: check-merge-conflict exclude: rst$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.8.6 hooks: - id: ruff args: [--fix] @@ -42,7 +42,7 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.0 + rev: v1.14.1 hooks: - id: mypy exclude: ^(docs|tests)/.* @@ -53,7 +53,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/rhysd/actionlint - rev: v1.7.5 + rev: v1.7.6 hooks: - id: actionlint-docker args: diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 934ec98c..816c4639 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,7 +1,7 @@ attrs==24.3.0 coverage==7.6.10 exceptiongroup==1.2.2 -hypothesis==6.123.2 +hypothesis==6.123.4 iniconfig==2.0.0 packaging==24.2 pluggy==1.5.0 diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 530d174e..607c1632 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -8,7 +8,7 @@ imagesize==1.4.1 Jinja2==3.1.5 MarkupSafe==3.0.2 packaging==24.2 -Pygments==2.18.0 +Pygments==2.19.1 requests==2.32.3 snowballstemmer==2.2.0 Sphinx==8.0.2 diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index aca0ea0f..a28c9fd3 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +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 `_ + 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 `_ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 8ad17c0a..2f028ae1 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -708,11 +708,12 @@ def scoped_event_loop( event_loop_policy, ) -> Iterator[asyncio.AbstractEventLoop]: new_loop_policy = event_loop_policy - with _temporary_event_loop_policy(new_loop_policy): - loop = _make_pytest_asyncio_loop(asyncio.new_event_loop()) + with ( + _temporary_event_loop_policy(new_loop_policy), + _provide_event_loop() as loop, + ): asyncio.set_event_loop(loop) yield loop - loop.close() # @pytest.fixture does not register the fixture anywhere, so pytest doesn't # know it exists. We work around this by attaching the fixture function to the @@ -1147,16 +1148,26 @@ def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector: def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: """Create an instance of the default event loop for each test case.""" new_loop_policy = request.getfixturevalue(event_loop_policy.__name__) - with _temporary_event_loop_policy(new_loop_policy): - loop = asyncio.get_event_loop_policy().new_event_loop() - # Add a magic value to the event loop, so pytest-asyncio can determine if the - # event_loop fixture was overridden. Other implementations of event_loop don't - # set this value. - # The magic value must be set as part of the function definition, because pytest - # seems to have multiple instances of the same FixtureDef or fixture function - loop = _make_pytest_asyncio_loop(loop) + with _temporary_event_loop_policy(new_loop_policy), _provide_event_loop() as loop: yield loop - loop.close() + + +@contextlib.contextmanager +def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]: + loop = asyncio.get_event_loop_policy().new_event_loop() + # Add a magic value to the event loop, so pytest-asyncio can determine if the + # event_loop fixture was overridden. Other implementations of event_loop don't + # set this value. + # The magic value must be set as part of the function definition, because pytest + # seems to have multiple instances of the same FixtureDef or fixture function + loop = _make_pytest_asyncio_loop(loop) + try: + yield loop + finally: + try: + loop.run_until_complete(loop.shutdown_asyncgens()) + finally: + loop.close() @pytest.fixture(scope="session") @@ -1164,11 +1175,9 @@ def _session_event_loop( request: FixtureRequest, event_loop_policy: AbstractEventLoopPolicy ) -> Iterator[asyncio.AbstractEventLoop]: new_loop_policy = event_loop_policy - with _temporary_event_loop_policy(new_loop_policy): - loop = _make_pytest_asyncio_loop(asyncio.new_event_loop()) + with _temporary_event_loop_policy(new_loop_policy), _provide_event_loop() as loop: asyncio.set_event_loop(loop) yield loop - loop.close() @pytest.fixture(scope="session", autouse=True) diff --git a/tests/test_event_loop_fixture.py b/tests/test_event_loop_fixture.py index 21785075..447d15d5 100644 --- a/tests/test_event_loop_fixture.py +++ b/tests/test_event_loop_fixture.py @@ -53,3 +53,30 @@ async def test_custom_policy_is_not_overwritten(): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=2) + + +def test_event_loop_fixture_handles_unclosed_async_gen( + 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(): + async def generator_fn(): + yield + yield + + gen = generator_fn() + await gen.__anext__() + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") + result.assert_outcomes(passed=1, warnings=0)