From b289f88866fd1df5172d0f17aa17a22c69cce23c Mon Sep 17 00:00:00 2001 From: Sahil Bhatti Date: Tue, 12 May 2026 23:11:02 +0530 Subject: [PATCH 1/4] Set default asyncio fixture loop scope to function --- changelog.d/1298.changed.rst | 2 +- docs/how-to-guides/migrate_from_0_21.rst | 2 +- docs/how-to-guides/migrate_from_0_23.rst | 2 +- pytest_asyncio/plugin.py | 14 +------------ tests/test_fixture_loop_scopes.py | 25 ++++++++++++++++++++++++ 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/changelog.d/1298.changed.rst b/changelog.d/1298.changed.rst index 49f83c83..1f3b5d1e 100644 --- a/changelog.d/1298.changed.rst +++ b/changelog.d/1298.changed.rst @@ -1 +1 @@ -Improved the readability of the warning message that is displayed when ``asyncio_default_fixture_loop_scope`` is unset +The default value of ``asyncio_default_fixture_loop_scope`` is now ``function``. The deprecated unset behavior and warning have been removed. diff --git a/docs/how-to-guides/migrate_from_0_21.rst b/docs/how-to-guides/migrate_from_0_21.rst index a244ad1f..4faadc7c 100644 --- a/docs/how-to-guides/migrate_from_0_21.rst +++ b/docs/how-to-guides/migrate_from_0_21.rst @@ -14,4 +14,4 @@ Go through all re-implemented *event_loop* fixtures in your test suite one by on 1. For all tests and fixtures affected by the re-implemented *event_loop* fixture, configure the *loop_scope* for async tests and fixtures to match the *event_loop* fixture scope. This can be done for each test and fixture individually using either the ``pytest.mark.asyncio(loop_scope="…")`` marker for async tests or ``@pytest_asyncio.fixture(loop_scope="…")`` for async fixtures. Alternatively, you can set the default loop scope for fixtures using the :ref:`asyncio_default_fixture_loop_scope ` configuration option. Snippets to mark all tests with the same *asyncio* marker, thus sharing the same loop scope, are present in the how-to section of the documentation. Depending on the homogeneity of your test suite, you may want a mixture of explicit decorators and default settings. 2. Remove the re-implemented *event_loop* fixture. -If you haven't set the *asyncio_default_fixture_loop_scope* configuration option, yet, set it to *function* to silence the deprecation warning. +The *asyncio_default_fixture_loop_scope* configuration option now defaults to *function* when unset. diff --git a/docs/how-to-guides/migrate_from_0_23.rst b/docs/how-to-guides/migrate_from_0_23.rst index 280b0a80..1e1743df 100644 --- a/docs/how-to-guides/migrate_from_0_23.rst +++ b/docs/how-to-guides/migrate_from_0_23.rst @@ -6,5 +6,5 @@ How to migrate from pytest-asyncio v0.23 The following steps assume that your test suite has no re-implementations of the *event_loop* fixture, nor explicit fixtures requests for it. If this isn't the case, please follow the :ref:`migration guide for pytest-asyncio v0.21. ` 1. Explicitly set the *loop_scope* of async fixtures by replacing occurrences of ``@pytest.fixture(scope="…")`` and ``@pytest_asyncio.fixture(scope="…")`` with ``@pytest_asyncio.fixture(loop_scope="…", scope="…")`` such that *loop_scope* and *scope* are the same. If you use auto mode, resolve all import errors from missing imports of *pytest_asyncio*. If your async fixtures all use the same *loop_scope*, you may choose to set the *asyncio_default_fixture_loop_scope* configuration option to that loop scope, instead. -2. If you haven't set *asyncio_default_fixture_loop_scope*, set it to *function* to address the deprecation warning about the unset configuration option. +2. The *asyncio_default_fixture_loop_scope* configuration option now defaults to *function* when unset. 3. Change all occurrences of ``pytest.mark.asyncio(scope="…")`` to ``pytest.mark.asyncio(loop_scope="…")`` to address the deprecation warning about the *scope* argument to the *asyncio* marker. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2fe8db12..3e950a73 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -137,7 +137,7 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None "asyncio_default_fixture_loop_scope", type="string", help="default scope of the asyncio event loop used to execute async fixtures", - default=None, + default="function", ) parser.addini( "asyncio_default_test_loop_scope", @@ -271,15 +271,6 @@ def _collect_hook_loop_factories( return factories -_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET = """\ -The configuration option "asyncio_default_fixture_loop_scope" is unset. -The event loop scope for asynchronous fixtures will default to the "fixture" caching \ -scope. Future versions of pytest-asyncio will default the loop scope for asynchronous \ -fixtures to "function" scope. Set the default fixture loop scope explicitly in order \ -to avoid unexpected behavior in the future. Valid fixture loop scopes are: \ -"function", "class", "module", "package", "session" -""" - def _validate_scope(scope: str | None, option_name: str) -> None: if scope is None: @@ -295,8 +286,6 @@ def _validate_scope(scope: str | None, option_name: str) -> None: 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") @@ -928,7 +917,6 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None: loop_scope = ( getattr(fixturedef.func, "_loop_scope", None) or default_loop_scope - or fixturedef.scope ) 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 a25e56a0..82b231b9 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -34,6 +34,31 @@ async def test_runs_in_same_loop_as_fixture(fixture): result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=1) +def test_default_fixture_loop_scope_is_function_when_unset(pytester: Pytester): + pytester.makepyfile( + dedent( + ''' + import pytest + import pytest_asyncio + + @pytest_asyncio.fixture + async def value(): + return 1 + + @pytest.mark.asyncio + async def test_value(value): + assert value == 1 + ''' + ) + ) + + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "error") + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + [ + "*asyncio_default_fixture_loop_scope=function*", + ] + ) @pytest.mark.parametrize("default_loop_scope", ("function", "module", "session")) def test_default_loop_scope_config_option_changes_fixture_loop_scope( From f846450abe3be8cb725f4e7a9271cea9667d0b2f Mon Sep 17 00:00:00 2001 From: Sahil Bhatti Date: Tue, 12 May 2026 23:20:04 +0530 Subject: [PATCH 2/4] Add changelog fragment for fixture loop scope default --- changelog.d/924.changed.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/924.changed.rst diff --git a/changelog.d/924.changed.rst b/changelog.d/924.changed.rst new file mode 100644 index 00000000..1f3b5d1e --- /dev/null +++ b/changelog.d/924.changed.rst @@ -0,0 +1 @@ +The default value of ``asyncio_default_fixture_loop_scope`` is now ``function``. The deprecated unset behavior and warning have been removed. From d70eb9701c6695aed6f0b58f3e0327d36e00b5f0 Mon Sep 17 00:00:00 2001 From: Sahil Bhatti Date: Tue, 12 May 2026 23:28:50 +0530 Subject: [PATCH 3/4] Apply shed formatting --- pytest_asyncio/plugin.py | 6 +----- tests/test_fixture_loop_scopes.py | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 3e950a73..afd1d2ed 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -271,7 +271,6 @@ def _collect_hook_loop_factories( return factories - def _validate_scope(scope: str | None, option_name: str) -> None: if scope is None: return @@ -914,10 +913,7 @@ 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 - ) + loop_scope = getattr(fixturedef.func, "_loop_scope", None) or default_loop_scope 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 82b231b9..378dfb1b 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -34,10 +34,9 @@ async def test_runs_in_same_loop_as_fixture(fixture): result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=1) + def test_default_fixture_loop_scope_is_function_when_unset(pytester: Pytester): - pytester.makepyfile( - dedent( - ''' + pytester.makepyfile(dedent(""" import pytest import pytest_asyncio @@ -48,17 +47,16 @@ async def value(): @pytest.mark.asyncio async def test_value(value): assert value == 1 - ''' - ) - ) - - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "error") - result.assert_outcomes(passed=1) - result.stdout.fnmatch_lines( - [ - "*asyncio_default_fixture_loop_scope=function*", - ] - ) + """)) + + result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "error") + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + [ + "*asyncio_default_fixture_loop_scope=function*", + ] + ) + @pytest.mark.parametrize("default_loop_scope", ("function", "module", "session")) def test_default_loop_scope_config_option_changes_fixture_loop_scope( From 7ec69642bbf9925be95f3499a424ec3d84c8a456 Mon Sep 17 00:00:00 2001 From: Sahil Bhatti Date: Fri, 15 May 2026 17:05:47 +0530 Subject: [PATCH 4/4] Address fixture loop scope review --- changelog.d/1298.changed.rst | 2 +- docs/how-to-guides/migrate_from_0_21.rst | 2 +- docs/how-to-guides/migrate_from_0_23.rst | 2 +- tests/test_fixture_loop_scopes.py | 17 ++++++++++------- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/changelog.d/1298.changed.rst b/changelog.d/1298.changed.rst index 1f3b5d1e..49f83c83 100644 --- a/changelog.d/1298.changed.rst +++ b/changelog.d/1298.changed.rst @@ -1 +1 @@ -The default value of ``asyncio_default_fixture_loop_scope`` is now ``function``. The deprecated unset behavior and warning have been removed. +Improved the readability of the warning message that is displayed when ``asyncio_default_fixture_loop_scope`` is unset diff --git a/docs/how-to-guides/migrate_from_0_21.rst b/docs/how-to-guides/migrate_from_0_21.rst index 4faadc7c..a244ad1f 100644 --- a/docs/how-to-guides/migrate_from_0_21.rst +++ b/docs/how-to-guides/migrate_from_0_21.rst @@ -14,4 +14,4 @@ Go through all re-implemented *event_loop* fixtures in your test suite one by on 1. For all tests and fixtures affected by the re-implemented *event_loop* fixture, configure the *loop_scope* for async tests and fixtures to match the *event_loop* fixture scope. This can be done for each test and fixture individually using either the ``pytest.mark.asyncio(loop_scope="…")`` marker for async tests or ``@pytest_asyncio.fixture(loop_scope="…")`` for async fixtures. Alternatively, you can set the default loop scope for fixtures using the :ref:`asyncio_default_fixture_loop_scope ` configuration option. Snippets to mark all tests with the same *asyncio* marker, thus sharing the same loop scope, are present in the how-to section of the documentation. Depending on the homogeneity of your test suite, you may want a mixture of explicit decorators and default settings. 2. Remove the re-implemented *event_loop* fixture. -The *asyncio_default_fixture_loop_scope* configuration option now defaults to *function* when unset. +If you haven't set the *asyncio_default_fixture_loop_scope* configuration option, yet, set it to *function* to silence the deprecation warning. diff --git a/docs/how-to-guides/migrate_from_0_23.rst b/docs/how-to-guides/migrate_from_0_23.rst index 1e1743df..280b0a80 100644 --- a/docs/how-to-guides/migrate_from_0_23.rst +++ b/docs/how-to-guides/migrate_from_0_23.rst @@ -6,5 +6,5 @@ How to migrate from pytest-asyncio v0.23 The following steps assume that your test suite has no re-implementations of the *event_loop* fixture, nor explicit fixtures requests for it. If this isn't the case, please follow the :ref:`migration guide for pytest-asyncio v0.21. ` 1. Explicitly set the *loop_scope* of async fixtures by replacing occurrences of ``@pytest.fixture(scope="…")`` and ``@pytest_asyncio.fixture(scope="…")`` with ``@pytest_asyncio.fixture(loop_scope="…", scope="…")`` such that *loop_scope* and *scope* are the same. If you use auto mode, resolve all import errors from missing imports of *pytest_asyncio*. If your async fixtures all use the same *loop_scope*, you may choose to set the *asyncio_default_fixture_loop_scope* configuration option to that loop scope, instead. -2. The *asyncio_default_fixture_loop_scope* configuration option now defaults to *function* when unset. +2. If you haven't set *asyncio_default_fixture_loop_scope*, set it to *function* to address the deprecation warning about the unset configuration option. 3. Change all occurrences of ``pytest.mark.asyncio(scope="…")`` to ``pytest.mark.asyncio(loop_scope="…")`` to address the deprecation warning about the *scope* argument to the *asyncio* marker. diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index 378dfb1b..43c45710 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -37,23 +37,26 @@ async def test_runs_in_same_loop_as_fixture(fixture): def test_default_fixture_loop_scope_is_function_when_unset(pytester: Pytester): pytester.makepyfile(dedent(""" + import asyncio import pytest import pytest_asyncio - @pytest_asyncio.fixture - async def value(): - return 1 + @pytest_asyncio.fixture(scope="module") + async def fixture_loop(): + return asyncio.get_running_loop() - @pytest.mark.asyncio - async def test_value(value): - assert value == 1 + @pytest.mark.asyncio(loop_scope="module") + async def test_fixture_uses_default_function_loop_scope(fixture_loop): + assert asyncio.get_running_loop() is fixture_loop """)) result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "error") - result.assert_outcomes(passed=1) + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( [ "*asyncio_default_fixture_loop_scope=function*", + "*ScopeMismatch*function scoped fixture _function_scoped_runner*" + "module scoped request object*", ] )