From 07c378e53abb7ea3bfa4d536694968ed4e1daf6b Mon Sep 17 00:00:00 2001 From: Mathew Kadambatt <49642721+mathewOracle@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:37:07 +0530 Subject: [PATCH 1/3] Use issue_config_time_warning for the unset-loop-scope deprecation `pytest_configure` warns via a plain `warnings.warn()` when `asyncio_default_fixture_loop_scope` is unset. Per pytest's own docs on `Config.issue_config_time_warning`, warnings raised this way during `pytest_configure` can't be captured the way per-test warnings are -- pytest has no way to install a hookwrapper around `pytest_configure` -- so the warning is silently dropped: it never reaches the warnings summary, and doesn't respect `-W`/`filterwarnings`. $ pytest # asyncio_default_fixture_loop_scope left unset 2 passed in 0.01s # no warning shown anywhere, including with -W default Depending on the caller's global warning filter state it can also raise an uncaught PytestDeprecationWarning straight out of pytest_configure, crashing the run outright (reproducible via pytester's in-process runpytest(), which shares the outer process's filters). `Config.issue_config_time_warning` is pytest's documented mechanism for exactly this case, and is what pytest's own core plugins use for their config-time deprecations (e.g. _pytest/pastebin.py). Switching to it makes the warning show up in the warnings summary and respect the caller's filters, with no change to the message or when it fires. Verified live against the exact repro from the issue: the warning now appears in a plain `pytest` run with no flags needed. Added a regression test that fails against the previous code (either silently, or via INTERNALERROR depending on inherited filters) and passes with this change, plus a sanity test that no warning fires when the option is configured. Full test suite shows identical pre-existing failures/errors before and after this change (unrelated compatibility gaps with the installed pytest version); no new failures introduced. ruff check clean. Closes #1142 --- pytest_asyncio/plugin.py | 11 +++++- tests/test_fixture_loop_scopes.py | 66 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 38b75e41..19797872 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -297,7 +297,16 @@ 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)) + # A plain warnings.warn() here is silently lost: pytest can't wrap + # pytest_configure with catch_warnings_for_item (no hookwrappers are + # possible around it), so the warning never reaches pytest's own + # recording/filtering and is dropped before the warnings summary. + # issue_config_time_warning is pytest's documented way to emit a + # warning during configure and have it actually surface; pytest's + # own core plugins (e.g. _pytest/pastebin.py) use the same pattern. + config.issue_config_time_warning( + PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET), stacklevel=2 + ) default_test_loop_scope = config.getini("asyncio_default_test_loop_scope") _validate_scope(default_test_loop_scope, "asyncio_default_test_loop_scope") diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index a25e56a0..ea71d5b3 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -113,6 +113,72 @@ async def test_runs_in_fixture_loop(fixture_loop): result.assert_outcomes(passed=1) +def test_unset_default_fixture_loop_scope_warning_is_reported(pytester: Pytester): + """ + A plain `warnings.warn()` call in `pytest_configure` is invisible to + pytest: pytest can't wrap `pytest_configure` with the hookwrapper that + normally records warnings, so the warning never reaches the warnings + summary or respects `-W`/`filterwarnings`, and silently vanishes. + + https://github.com/pytest-dev/pytest-asyncio/issues/1142 + """ + pytester.makepyfile(dedent("""\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + + @pytest_asyncio.fixture + async def fixt() -> None: + yield + + + @pytest.mark.asyncio(loop_scope="session") + async def test_a(): + global loop + loop = asyncio.get_running_loop() + + + @pytest.mark.asyncio(loop_scope="session") + async def test_b(fixt): + assert asyncio.get_running_loop() is loop + """)) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2, warnings=1) + result.stdout.fnmatch_lines( + [ + "*warnings summary*", + ( + "*PytestDeprecationWarning: The configuration option " + '"asyncio_default_fixture_loop_scope" is unset.*' + ), + ] + ) + + +def test_configured_default_fixture_loop_scope_has_no_warning(pytester: Pytester): + """Sanity check: the warning above is specific to the unset case.""" + pytester.makeini("""\ + [pytest] + asyncio_default_fixture_loop_scope = function + """) + pytester.makepyfile(dedent("""\ + import pytest_asyncio + + @pytest_asyncio.fixture + async def fixt() -> None: + yield + + async def test_it(fixt) -> None: + pass + """)) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1, warnings=0) + + def test_invalid_default_fixture_loop_scope_raises_error(pytester: Pytester): pytester.makeini("""\ [pytest] From fe2042613b805dffd221c296302b0a159a6b7f3f Mon Sep 17 00:00:00 2001 From: Mathew Kadambatt <49642721+mathewOracle@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:25:45 +0530 Subject: [PATCH 2/3] Add changelog.d fragment for #1142 --- changelog.d/1142.fixed.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1142.fixed.rst diff --git a/changelog.d/1142.fixed.rst b/changelog.d/1142.fixed.rst new file mode 100644 index 00000000..0bbcbd2e --- /dev/null +++ b/changelog.d/1142.fixed.rst @@ -0,0 +1 @@ +Fix the deprecation warning for an unset `asyncio_default_fixture_loop_scope` not being reported. The warning is now issued via `Config.issue_config_time_warning`, so it correctly appears in the warnings summary and respects `-W`/`filterwarnings`. From 6014a08ee2b900ed26a930f21a200907d209832c Mon Sep 17 00:00:00 2001 From: mathewOracle <49642721+mathewOracle@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:56:53 +0530 Subject: [PATCH 3/3] Address review: trim comment, simplify tests, reword changelog - Drop the PR-commentary comment in pytest_configure. - Replace the broad loop-scope tests with focused ones: the warning shows up in the summary, and is silenceable via -W and filterwarnings. - Reword the changelog fragment to describe user-visible behaviour rather than the Config.issue_config_time_warning implementation detail. --- changelog.d/1142.fixed.rst | 2 +- pytest_asyncio/plugin.py | 7 --- tests/test_fixture_loop_scopes.py | 92 +++++++++++++------------------ 3 files changed, 38 insertions(+), 63 deletions(-) diff --git a/changelog.d/1142.fixed.rst b/changelog.d/1142.fixed.rst index 0bbcbd2e..40a917ec 100644 --- a/changelog.d/1142.fixed.rst +++ b/changelog.d/1142.fixed.rst @@ -1 +1 @@ -Fix the deprecation warning for an unset `asyncio_default_fixture_loop_scope` not being reported. The warning is now issued via `Config.issue_config_time_warning`, so it correctly appears in the warnings summary and respects `-W`/`filterwarnings`. +The deprecation warning for an unset `asyncio_default_fixture_loop_scope` is now visible to pytest: it appears in the warnings summary and can be filtered with `-W` and `filterwarnings`. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 19797872..3060aae5 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -297,13 +297,6 @@ 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: - # A plain warnings.warn() here is silently lost: pytest can't wrap - # pytest_configure with catch_warnings_for_item (no hookwrappers are - # possible around it), so the warning never reaches pytest's own - # recording/filtering and is dropped before the warnings summary. - # issue_config_time_warning is pytest's documented way to emit a - # warning during configure and have it actually surface; pytest's - # own core plugins (e.g. _pytest/pastebin.py) use the same pattern. config.issue_config_time_warning( PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET), stacklevel=2 ) diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index ea71d5b3..6b4f6707 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -113,70 +113,52 @@ async def test_runs_in_fixture_loop(fixture_loop): result.assert_outcomes(passed=1) -def test_unset_default_fixture_loop_scope_warning_is_reported(pytester: Pytester): +_UNSET_FIXTURE_LOOP_SCOPE_WARNING = ( + "*PytestDeprecationWarning: The configuration option " + '"asyncio_default_fixture_loop_scope" is unset.*' +) + + +def test_unset_default_fixture_loop_scope_warning_appears_in_summary( + pytester: Pytester, +): """ - A plain `warnings.warn()` call in `pytest_configure` is invisible to - pytest: pytest can't wrap `pytest_configure` with the hookwrapper that - normally records warnings, so the warning never reaches the warnings - summary or respects `-W`/`filterwarnings`, and silently vanishes. + The warning is emitted during configure, so it must still be recorded. https://github.com/pytest-dev/pytest-asyncio/issues/1142 """ - pytester.makepyfile(dedent("""\ - import asyncio - - import pytest - import pytest_asyncio - - loop: asyncio.AbstractEventLoop - - - @pytest_asyncio.fixture - async def fixt() -> None: - yield - - - @pytest.mark.asyncio(loop_scope="session") - async def test_a(): - global loop - loop = asyncio.get_running_loop() - - - @pytest.mark.asyncio(loop_scope="session") - async def test_b(fixt): - assert asyncio.get_running_loop() is loop - """)) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=2, warnings=1) + pytester.makepyfile("async def test_it(): pass") + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1, warnings=1) result.stdout.fnmatch_lines( - [ - "*warnings summary*", - ( - "*PytestDeprecationWarning: The configuration option " - '"asyncio_default_fixture_loop_scope" is unset.*' - ), - ] + ["*warnings summary*", _UNSET_FIXTURE_LOOP_SCOPE_WARNING] ) -def test_configured_default_fixture_loop_scope_has_no_warning(pytester: Pytester): - """Sanity check: the warning above is specific to the unset case.""" - pytester.makeini("""\ - [pytest] - asyncio_default_fixture_loop_scope = function - """) - pytester.makepyfile(dedent("""\ - import pytest_asyncio - - @pytest_asyncio.fixture - async def fixt() -> None: - yield - - async def test_it(fixt) -> None: - pass - """)) - result = pytester.runpytest("--asyncio-mode=auto") +@pytest.mark.parametrize( + ("ini", "args"), + ( + pytest.param( + "", ("-Wignore::pytest.PytestDeprecationWarning",), id="command-line-W" + ), + pytest.param( + "filterwarnings = ignore::pytest.PytestDeprecationWarning", + (), + id="filterwarnings-ini", + ), + ), +) +def test_unset_default_fixture_loop_scope_warning_is_filterable( + pytester: Pytester, + ini: str, + args: tuple[str, ...], +): + """Being recorded properly also means users can silence it.""" + pytester.makeini(f"[pytest]\n{ini}") + pytester.makepyfile("async def test_it(): pass") + result = pytester.runpytest("--asyncio-mode=auto", *args) result.assert_outcomes(passed=1, warnings=0) + result.stdout.no_fnmatch_line(_UNSET_FIXTURE_LOOP_SCOPE_WARNING) def test_invalid_default_fixture_loop_scope_raises_error(pytester: Pytester):