From d0951dcc92727a22199e7b47ed421c4f8a47f7c3 Mon Sep 17 00:00:00 2001 From: pctablet505 Date: Wed, 15 Jul 2026 10:52:18 +0000 Subject: [PATCH 1/4] fix: only warn about asyncio_default_fixture_loop_scope when needed Fixes #1344 --- changelog.d/1344.fixed.rst | 1 + pytest_asyncio/plugin.py | 15 +++++++++-- tests/test_fixture_loop_scopes.py | 45 +++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 changelog.d/1344.fixed.rst diff --git a/changelog.d/1344.fixed.rst b/changelog.d/1344.fixed.rst new file mode 100644 index 00000000..4982e915 --- /dev/null +++ b/changelog.d/1344.fixed.rst @@ -0,0 +1 @@ +Only warn about an unset ``asyncio_default_fixture_loop_scope`` configuration option when an async fixture is actually used. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 38b75e41..53222d69 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -293,11 +293,20 @@ def _validate_scope(scope: str | None, option_name: str) -> None: ) +_default_fixture_loop_scope_warning_emitted_for_config: set[int] = set() + + +def _warn_default_fixture_loop_scope_unset(config: Config) -> None: + config_id = id(config) + if config_id in _default_fixture_loop_scope_warning_emitted_for_config: + return + _default_fixture_loop_scope_warning_emitted_for_config.add(config_id) + warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET)) + + def pytest_configure(config: Config) -> None: default_fixture_loop_scope = config.getini("asyncio_default_fixture_loop_scope") _validate_scope(default_fixture_loop_scope, "asyncio_default_fixture_loop_scope") - if not default_fixture_loop_scope: - warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET)) default_test_loop_scope = config.getini("asyncio_default_test_loop_scope") _validate_scope(default_test_loop_scope, "asyncio_default_test_loop_scope") @@ -931,6 +940,8 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None: or default_loop_scope or fixturedef.scope ) + if not default_loop_scope and not getattr(fixturedef.func, "_loop_scope", None): + _warn_default_fixture_loop_scope_unset(request.config) runner_fixture_id = f"_{loop_scope}_scoped_runner" runner = request.getfixturevalue(runner_fixture_id) # Prevent the runner closing before the fixture's async teardown. diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index a25e56a0..3e1be6e8 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -126,3 +126,48 @@ def test_invalid_default_fixture_loop_scope_raises_error(pytester: Pytester): "function, class, module, package, session." ] ) + + +def test_no_warning_for_unset_default_fixture_loop_scope_without_async_fixtures( + pytester: Pytester, +): + """Regression test for #1344.""" + pytester.makeini(dedent("""\ + [pytest] + asyncio_mode = strict + """)) + pytester.makepyfile(dedent("""\ + def test_no_async(): + pass + """)) + result = pytester.runpytest_subprocess("-Werror") + result.assert_outcomes(passed=1) + + +def test_warning_for_unset_default_fixture_loop_scope_with_async_fixture( + pytester: Pytester, +): + pytester.makeini(dedent("""\ + [pytest] + asyncio_mode = strict + """)) + pytester.makepyfile(dedent("""\ + import pytest + import pytest_asyncio + + @pytest_asyncio.fixture + async def async_fixture(): + return 42 + + @pytest.mark.asyncio + async def test_async(async_fixture): + assert async_fixture == 42 + """)) + result = pytester.runpytest_subprocess("-Wdefault") + result.assert_outcomes(passed=1, warnings=1) + result.stdout.fnmatch_lines( + [ + "*PytestDeprecationWarning: The configuration option " + "\"asyncio_default_fixture_loop_scope\" is unset.*" + ] + ) From 644518256241fcd864df538fa1ab953924cac88a Mon Sep 17 00:00:00 2001 From: pctablet505 Date: Wed, 15 Jul 2026 11:06:11 +0000 Subject: [PATCH 2/4] style: apply formatter to #1344 regression tests --- pytest_asyncio/plugin.py | 9 +++------ tests/test_fixture_loop_scopes.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 53222d69..4c22f684 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -935,12 +935,9 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None: if not _is_coroutine_or_asyncgen(fixturedef.func): return (yield) default_loop_scope = request.config.getini("asyncio_default_fixture_loop_scope") - loop_scope = ( - getattr(fixturedef.func, "_loop_scope", None) - or default_loop_scope - or fixturedef.scope - ) - if not default_loop_scope and not getattr(fixturedef.func, "_loop_scope", None): + fixture_loop_scope = getattr(fixturedef.func, "_loop_scope", None) + loop_scope = fixture_loop_scope or default_loop_scope or fixturedef.scope + if not default_loop_scope and not fixture_loop_scope: _warn_default_fixture_loop_scope_unset(request.config) runner_fixture_id = f"_{loop_scope}_scoped_runner" runner = request.getfixturevalue(runner_fixture_id) diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index 3e1be6e8..2447d202 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -168,6 +168,6 @@ async def test_async(async_fixture): result.stdout.fnmatch_lines( [ "*PytestDeprecationWarning: The configuration option " - "\"asyncio_default_fixture_loop_scope\" is unset.*" + '"asyncio_default_fixture_loop_scope" is unset.*' ] ) From 3832ee176d476f357556c0256d69c243a68f7945 Mon Sep 17 00:00:00 2001 From: pctablet505 Date: Wed, 15 Jul 2026 11:12:43 +0000 Subject: [PATCH 3/4] refactor: store warning-emitted flag on Config object --- pytest_asyncio/plugin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 4c22f684..baf847c8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -293,14 +293,10 @@ def _validate_scope(scope: str | None, option_name: str) -> None: ) -_default_fixture_loop_scope_warning_emitted_for_config: set[int] = set() - - def _warn_default_fixture_loop_scope_unset(config: Config) -> None: - config_id = id(config) - if config_id in _default_fixture_loop_scope_warning_emitted_for_config: + if getattr(config, "_asyncio_default_fixture_loop_scope_warning_emitted", False): return - _default_fixture_loop_scope_warning_emitted_for_config.add(config_id) + config._asyncio_default_fixture_loop_scope_warning_emitted = True warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET)) From 95da65afa292d09fc730b48a18a17a7c4aa57369 Mon Sep 17 00:00:00 2001 From: pctablet505 Date: Wed, 15 Jul 2026 11:37:37 +0000 Subject: [PATCH 4/4] Replace Config monkey-patch with pytest StashKey for warning deduplication --- pytest_asyncio/plugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index baf847c8..13110d3a 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -52,6 +52,7 @@ PytestCollectionWarning, PytestDeprecationWarning, PytestPluginManager, + StashKey, ) if sys.version_info >= (3, 11): @@ -293,10 +294,13 @@ def _validate_scope(scope: str | None, option_name: str) -> None: ) +_DEFAULT_FIXTURE_LOOP_SCOPE_WARNING_EMITTED: StashKey[bool] = StashKey() + + def _warn_default_fixture_loop_scope_unset(config: Config) -> None: - if getattr(config, "_asyncio_default_fixture_loop_scope_warning_emitted", False): + if config.stash.get(_DEFAULT_FIXTURE_LOOP_SCOPE_WARNING_EMITTED, False): return - config._asyncio_default_fixture_loop_scope_warning_emitted = True + config.stash[_DEFAULT_FIXTURE_LOOP_SCOPE_WARNING_EMITTED] = True warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))