From d83de5ac05295383543d0f48d3e23c028646c754 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 5 Jul 2024 14:07:59 +0200 Subject: [PATCH 01/11] [feat] Added optional "loop_scope" argument to pytest_asyncio.fixture. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 19 +++++++++++++------ tests/test_fixture_loop_scopes.py | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 tests/test_fixture_loop_scopes.py diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 04a7faca..22247c96 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -110,6 +110,7 @@ def fixture( fixture_function: FixtureFunction, *, scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., + loop_scope: Union[_ScopeName, None] = ..., params: Optional[Iterable[object]] = ..., autouse: bool = ..., ids: Union[ @@ -126,6 +127,7 @@ def fixture( fixture_function: None = ..., *, scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., + loop_scope: Union[_ScopeName, None] = ..., params: Optional[Iterable[object]] = ..., autouse: bool = ..., ids: Union[ @@ -138,17 +140,19 @@ def fixture( def fixture( - fixture_function: Optional[FixtureFunction] = None, **kwargs: Any + fixture_function: Optional[FixtureFunction] = None, + loop_scope: Union[_ScopeName, None] = None, + **kwargs: Any, ) -> Union[FixtureFunction, FixtureFunctionMarker]: if fixture_function is not None: - _make_asyncio_fixture_function(fixture_function) + _make_asyncio_fixture_function(fixture_function, loop_scope) return pytest.fixture(fixture_function, **kwargs) else: @functools.wraps(fixture) def inner(fixture_function: FixtureFunction) -> FixtureFunction: - return fixture(fixture_function, **kwargs) + return fixture(fixture_function, loop_scope=loop_scope, **kwargs) return inner @@ -158,11 +162,14 @@ def _is_asyncio_fixture_function(obj: Any) -> bool: return getattr(obj, "_force_asyncio_fixture", False) -def _make_asyncio_fixture_function(obj: Any) -> None: +def _make_asyncio_fixture_function( + obj: Any, loop_scope: Union[_ScopeName, None] +) -> None: if hasattr(obj, "__func__"): # instance method, check the function object obj = obj.__func__ obj._force_asyncio_fixture = True + obj._loop_scope = loop_scope def _is_coroutine_or_asyncgen(obj: Any) -> bool: @@ -218,7 +225,7 @@ def _preprocess_async_fixtures( # Ignore async fixtures without explicit asyncio mark in strict mode # This applies to pytest_trio fixtures, for example continue - scope = fixturedef.scope + scope = getattr(func, "_loop_scope", None) or fixturedef.scope if scope == "function": event_loop_fixture_id: Optional[str] = "event_loop" else: @@ -228,7 +235,7 @@ def _preprocess_async_fixtures( _event_loop_fixture_id, # type: ignore[arg-type] None, ) - _make_asyncio_fixture_function(func) + _make_asyncio_fixture_function(func, scope) function_signature = inspect.signature(func) if "event_loop" in function_signature.parameters: warnings.warn( diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py new file mode 100644 index 00000000..91c5a089 --- /dev/null +++ b/tests/test_fixture_loop_scopes.py @@ -0,0 +1,19 @@ +import asyncio + +import pytest + +import pytest_asyncio + +loop: asyncio.AbstractEventLoop + + +@pytest_asyncio.fixture(loop_scope="session") +async def fixture(): + global loop + loop = asyncio.get_running_loop() + + +@pytest.mark.asyncio(scope="session") +async def test_fixture_loop_scopes(fixture): + global loop + assert loop == asyncio.get_running_loop() From 2c3e1982a034c7168051d1202f452a07fe1484d8 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 5 Jul 2024 14:35:49 +0200 Subject: [PATCH 02/11] [test] Test the "loop_scope=session" argument to pytest_asyncio.fixture with additional pytest fixture scopes. Signed-off-by: Michael Seifert --- tests/test_fixture_loop_scopes.py | 41 ++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index 91c5a089..1f4d0a35 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -1,19 +1,36 @@ -import asyncio +from textwrap import dedent import pytest +from pytest import Pytester -import pytest_asyncio -loop: asyncio.AbstractEventLoop +@pytest.mark.parametrize( + "fixture_scope", ("session", "package", "module", "class", "function") +) +def test_loop_scope_session_is_independent_of_fixture_scope( + pytester: Pytester, + fixture_scope: str, +): + pytester.makepyfile( + dedent( + f"""\ + import asyncio + import pytest + import pytest_asyncio + loop: asyncio.AbstractEventLoop = None -@pytest_asyncio.fixture(loop_scope="session") -async def fixture(): - global loop - loop = asyncio.get_running_loop() + @pytest_asyncio.fixture(scope="{fixture_scope}", loop_scope="session") + async def fixture(): + global loop + loop = asyncio.get_running_loop() - -@pytest.mark.asyncio(scope="session") -async def test_fixture_loop_scopes(fixture): - global loop - assert loop == asyncio.get_running_loop() + @pytest.mark.asyncio(scope="session") + async def test_runs_in_same_loop_as_fixture(fixture): + global loop + assert loop == asyncio.get_running_loop() + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) From 7369b75f16bff361cd55d2c9a99a54ab1fba65f6 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 9 Jul 2024 08:15:16 +0200 Subject: [PATCH 03/11] [refactor] Improve exception handling in pytest_pycollect_makeitem_convert_async_functions_to_subclass. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 22247c96..36c2dbf5 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -542,9 +542,16 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( to AsyncFunction items. """ hook_result = yield - node_or_list_of_nodes: Union[ - pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]], None - ] = hook_result.get_result() + try: + node_or_list_of_nodes: Union[ + pytest.Item, + pytest.Collector, + List[Union[pytest.Item, pytest.Collector]], + None, + ] = hook_result.get_result() + except BaseException as e: + hook_result.force_exception(e) + return if not node_or_list_of_nodes: return if isinstance(node_or_list_of_nodes, Sequence): From 7f40eeeafec48578bac7752b0ea277e26f4a0cdb Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Tue, 9 Jul 2024 13:17:53 +0200 Subject: [PATCH 04/11] [feat] Added the asyncio_default_fixture_loop_scope configuration option. Signed-off-by: Michael Seifert --- docs/source/reference/configuration.rst | 6 ++ pytest_asyncio/plugin.py | 30 ++++++- tests/test_fixture_loop_scopes.py | 100 ++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 3 deletions(-) diff --git a/docs/source/reference/configuration.rst b/docs/source/reference/configuration.rst index 5d840c47..7b0d1f95 100644 --- a/docs/source/reference/configuration.rst +++ b/docs/source/reference/configuration.rst @@ -2,6 +2,12 @@ Configuration ============= +asyncio_default_fixture_loop_scope +================================== +Determines the default event loop scope of asynchronous fixtures. When this configuration option is unset, it defaults to the fixture scope. In future versions of pytest-asyncio, the value will default to ``function`` when unset. Possible values are: ``function``, ``class``, ``module``, ``package``, ``session`` + +asyncio_mode +============ The pytest-asyncio mode can be set by the ``asyncio_mode`` configuration option in the `configuration file `_: diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 36c2dbf5..d7618c1b 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -103,6 +103,12 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None help="default value for --asyncio-mode", default="strict", ) + parser.addini( + "asyncio_default_fixture_loop_scope", + type="string", + help="default scope of the asyncio event loop used to execute async fixtures", + default=None, + ) @overload @@ -189,8 +195,20 @@ def _get_asyncio_mode(config: Config) -> Mode: ) +_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 pytest_configure(config: Config) -> None: - """Inject documentation.""" + default_loop_scope = config.getini("asyncio_default_fixture_loop_scope") + if not default_loop_scope: + warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET)) config.addinivalue_line( "markers", "asyncio: " @@ -203,7 +221,8 @@ def pytest_configure(config: Config) -> None: def pytest_report_header(config: Config) -> List[str]: """Add asyncio config to pytest header.""" mode = _get_asyncio_mode(config) - return [f"asyncio: mode={mode}"] + default_loop_scope = config.getini("asyncio_default_fixture_loop_scope") + return [f"asyncio: mode={mode}, default_loop_scope={default_loop_scope}"] def _preprocess_async_fixtures( @@ -211,6 +230,7 @@ def _preprocess_async_fixtures( processed_fixturedefs: Set[FixtureDef], ) -> None: config = collector.config + default_loop_scope = config.getini("asyncio_default_fixture_loop_scope") asyncio_mode = _get_asyncio_mode(config) fixturemanager = config.pluginmanager.get_plugin("funcmanage") assert fixturemanager is not None @@ -225,7 +245,11 @@ def _preprocess_async_fixtures( # Ignore async fixtures without explicit asyncio mark in strict mode # This applies to pytest_trio fixtures, for example continue - scope = getattr(func, "_loop_scope", None) or fixturedef.scope + scope = ( + getattr(func, "_loop_scope", None) + or default_loop_scope + or fixturedef.scope + ) if scope == "function": event_loop_fixture_id: Optional[str] = "event_loop" else: diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index 1f4d0a35..737a5573 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -34,3 +34,103 @@ async def test_runs_in_same_loop_as_fixture(fixture): ) result = pytester.runpytest_subprocess("--asyncio-mode=strict") result.assert_outcomes(passed=1) + + +@pytest.mark.parametrize("default_loop_scope", ("function", "module", "session")) +def test_default_loop_scope_config_option_changes_fixture_loop_scope( + pytester: Pytester, + default_loop_scope: str, +): + pytester.makeini( + dedent( + f"""\ + [pytest] + asyncio_default_fixture_loop_scope = {default_loop_scope} + """ + ) + ) + pytester.makepyfile( + dedent( + f"""\ + import asyncio + import pytest + import pytest_asyncio + + @pytest_asyncio.fixture + async def fixture_loop(): + return asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="{default_loop_scope}") + async def test_runs_in_fixture_loop(fixture_loop): + assert asyncio.get_running_loop() is fixture_loop + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_default_class_loop_scope_config_option_changes_fixture_loop_scope( + pytester: Pytester, +): + pytester.makeini( + dedent( + """\ + [pytest] + asyncio_default_fixture_loop_scope = class + """ + ) + ) + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + import pytest_asyncio + + class TestClass: + @pytest_asyncio.fixture + async def fixture_loop(self): + return asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="class") + async def test_runs_in_fixture_loop(self, fixture_loop): + assert asyncio.get_running_loop() is fixture_loop + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_default_package_loop_scope_config_option_changes_fixture_loop_scope( + pytester: Pytester, +): + pytester.makeini( + dedent( + """\ + [pytest] + asyncio_default_fixture_loop_scope = package + """ + ) + ) + pytester.makepyfile( + __init__="", + test_a=dedent( + """\ + import asyncio + import pytest + import pytest_asyncio + + @pytest_asyncio.fixture + async def fixture_loop(): + return asyncio.get_running_loop() + + @pytest.mark.asyncio(scope="package") + async def test_runs_in_fixture_loop(fixture_loop): + assert asyncio.get_running_loop() is fixture_loop + """ + ), + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) From 4491648df4bd93c886e686829160f33246099852 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 12 Jul 2024 13:21:45 +0200 Subject: [PATCH 05/11] refactor: Extracted function that returns the event loop scope of a marked function. --- pytest_asyncio/plugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index d7618c1b..766cfff8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -38,6 +38,7 @@ FixtureRequest, Function, Item, + Mark, Metafunc, Module, Package, @@ -738,7 +739,7 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: marker = metafunc.definition.get_closest_marker("asyncio") if not marker: return - scope = marker.kwargs.get("scope", "function") + scope = _get_marked_loop_scope(marker) if scope == "function": return event_loop_node = _retrieve_scope_root(metafunc.definition, scope) @@ -971,7 +972,7 @@ def pytest_runtest_setup(item: pytest.Item) -> None: marker = item.get_closest_marker("asyncio") if marker is None: return - scope = marker.kwargs.get("scope", "function") + scope = _get_marked_loop_scope(marker) if scope != "function": parent_node = _retrieve_scope_root(item, scope) event_loop_fixture_id = parent_node.stash[_event_loop_fixture_id] @@ -990,6 +991,11 @@ def pytest_runtest_setup(item: pytest.Item) -> None: ) +def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName: + assert asyncio_marker.name == "asyncio" + return asyncio_marker.kwargs.get("scope", "function") + + def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector: node_type_by_scope = { "class": Class, From 64e35775cf39e8757d99e956dd05214f1c6869ee Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 12 Jul 2024 13:49:36 +0200 Subject: [PATCH 06/11] feat: Allow use of "loop_scope" kwarg in asyncio markers. --- pytest_asyncio/plugin.py | 14 +++++++++- tests/markers/test_function_scope.py | 41 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 766cfff8..e8f6d4d8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -991,9 +991,21 @@ def pytest_runtest_setup(item: pytest.Item) -> None: ) +_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR = """\ +An asyncio pytest marker defines both "scope" and "loop_scope", \ +but it should only use "loop_scope". +""" + + def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName: assert asyncio_marker.name == "asyncio" - return asyncio_marker.kwargs.get("scope", "function") + if "scope" in asyncio_marker.kwargs and "loop_scope" in asyncio_marker.kwargs: + raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR) + scope = asyncio_marker.kwargs.get("loop_scope") or asyncio_marker.kwargs.get( + "scope", "function" + ) + assert scope in {"function", "class", "module", "package", "session"} + return scope def _retrieve_scope_root(item: Union[Collector, Item], scope: str) -> Collector: diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index eded4552..35771d93 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -28,6 +28,47 @@ async def test_does_not_run_in_same_loop(): result.assert_outcomes(passed=2) +def test_loop_scope_function_provides_function_scoped_event_loop(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + pytestmark = pytest.mark.asyncio(loop_scope="function") + + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + async def test_does_not_run_in_same_loop(): + global loop + assert asyncio.get_running_loop() is not loop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_raises_when_scope_and_loop_scope_arguments_are_present(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio(scope="function", loop_scope="function") + async def test_raises(): + ... + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(errors=1) + + def test_function_scope_supports_explicit_event_loop_fixture_request( pytester: Pytester, ): From 061532592f6fd80ee31139ddeee1d1b6c5472749 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 13 Jul 2024 15:24:08 +0200 Subject: [PATCH 07/11] [feat] Deprecates the optional `scope` keyword argument of asyncio markers. Users are encouraged to use the `loop_scope` keyword argument. The `loop_scope` kwarg does exactly the same, though its naming is consistent with the `loop_scope` kwarg of ``pytest_asyncio.fixture``. Signed-off-by: Michael Seifert --- docs/source/concepts.rst | 2 +- docs/source/concepts_module_scope_example.py | 4 +-- .../class_scoped_loop_example.py | 2 +- .../module_scoped_loop_example.py | 2 +- .../package_scoped_loop_example.py | 2 +- .../run_class_tests_in_same_loop.rst | 2 +- .../run_module_tests_in_same_loop.rst | 2 +- .../run_package_tests_in_same_loop.rst | 2 +- .../run_session_tests_in_same_loop.rst | 2 +- .../session_scoped_loop_example.py | 2 +- docs/source/reference/changelog.rst | 5 ++++ .../fixtures/event_loop_policy_example.py | 2 +- .../class_scoped_loop_strict_mode_example.py | 2 +- ...d_loop_with_fixture_strict_mode_example.py | 2 +- docs/source/reference/markers/index.rst | 2 +- .../module_scoped_loop_strict_mode_example.py | 2 +- pytest_asyncio/plugin.py | 11 ++++++-- .../test_async_fixtures_with_finalizer.py | 4 +-- tests/markers/test_class_scope.py | 20 +++++++------- tests/markers/test_function_scope.py | 17 ++++++++++++ tests/markers/test_module_scope.py | 22 ++++++++-------- tests/markers/test_package_scope.py | 24 ++++++++--------- tests/markers/test_session_scope.py | 26 +++++++++---------- tests/test_fixture_loop_scopes.py | 8 +++--- 24 files changed, 99 insertions(+), 70 deletions(-) diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst index 710c5365..298d08f2 100644 --- a/docs/source/concepts.rst +++ b/docs/source/concepts.rst @@ -32,7 +32,7 @@ You may notice that the individual levels resemble the possible `scopes of a pyt Pytest-asyncio provides one asyncio event loop for each pytest collector. By default, each test runs in the event loop provided by the *Function* collector, i.e. tests use the loop with the narrowest scope. This gives the highest level of isolation between tests. -If two or more tests share a common ancestor collector, the tests can be configured to run in their ancestor's loop by passing the appropriate *scope* keyword argument to the *asyncio* mark. +If two or more tests share a common ancestor collector, the tests can be configured to run in their ancestor's loop by passing the appropriate *loop_scope* keyword argument to the *asyncio* mark. For example, the following two tests use the asyncio event loop provided by the *Module* collector: .. include:: concepts_module_scope_example.py diff --git a/docs/source/concepts_module_scope_example.py b/docs/source/concepts_module_scope_example.py index 66972888..b83181b4 100644 --- a/docs/source/concepts_module_scope_example.py +++ b/docs/source/concepts_module_scope_example.py @@ -5,13 +5,13 @@ loop: asyncio.AbstractEventLoop -@pytest.mark.asyncio(scope="module") +@pytest.mark.asyncio(loop_scope="module") async def test_remember_loop(): global loop loop = asyncio.get_running_loop() -@pytest.mark.asyncio(scope="module") +@pytest.mark.asyncio(loop_scope="module") async def test_runs_in_a_loop(): global loop assert asyncio.get_running_loop() is loop diff --git a/docs/source/how-to-guides/class_scoped_loop_example.py b/docs/source/how-to-guides/class_scoped_loop_example.py index 5419a7ab..7ffc4b1f 100644 --- a/docs/source/how-to-guides/class_scoped_loop_example.py +++ b/docs/source/how-to-guides/class_scoped_loop_example.py @@ -3,7 +3,7 @@ import pytest -@pytest.mark.asyncio(scope="class") +@pytest.mark.asyncio(loop_scope="class") class TestInOneEventLoopPerClass: loop: asyncio.AbstractEventLoop diff --git a/docs/source/how-to-guides/module_scoped_loop_example.py b/docs/source/how-to-guides/module_scoped_loop_example.py index b4ef778c..38ba8bdc 100644 --- a/docs/source/how-to-guides/module_scoped_loop_example.py +++ b/docs/source/how-to-guides/module_scoped_loop_example.py @@ -2,7 +2,7 @@ import pytest -pytestmark = pytest.mark.asyncio(scope="module") +pytestmark = pytest.mark.asyncio(loop_scope="module") loop: asyncio.AbstractEventLoop diff --git a/docs/source/how-to-guides/package_scoped_loop_example.py b/docs/source/how-to-guides/package_scoped_loop_example.py index f48c33f1..903e9c8c 100644 --- a/docs/source/how-to-guides/package_scoped_loop_example.py +++ b/docs/source/how-to-guides/package_scoped_loop_example.py @@ -1,3 +1,3 @@ import pytest -pytestmark = pytest.mark.asyncio(scope="package") +pytestmark = pytest.mark.asyncio(loop_scope="package") diff --git a/docs/source/how-to-guides/run_class_tests_in_same_loop.rst b/docs/source/how-to-guides/run_class_tests_in_same_loop.rst index a265899c..2ba40683 100644 --- a/docs/source/how-to-guides/run_class_tests_in_same_loop.rst +++ b/docs/source/how-to-guides/run_class_tests_in_same_loop.rst @@ -1,7 +1,7 @@ ====================================================== How to run all tests in a class in the same event loop ====================================================== -All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="class")``. +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="class")``. This is easily achieved by using the *asyncio* marker as a class decorator. .. include:: class_scoped_loop_example.py diff --git a/docs/source/how-to-guides/run_module_tests_in_same_loop.rst b/docs/source/how-to-guides/run_module_tests_in_same_loop.rst index e07eca2e..c07de737 100644 --- a/docs/source/how-to-guides/run_module_tests_in_same_loop.rst +++ b/docs/source/how-to-guides/run_module_tests_in_same_loop.rst @@ -1,7 +1,7 @@ ======================================================= How to run all tests in a module in the same event loop ======================================================= -All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="module")``. +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="module")``. This is easily achieved by adding a `pytestmark` statement to your module. .. include:: module_scoped_loop_example.py diff --git a/docs/source/how-to-guides/run_package_tests_in_same_loop.rst b/docs/source/how-to-guides/run_package_tests_in_same_loop.rst index 24326ed1..0392693f 100644 --- a/docs/source/how-to-guides/run_package_tests_in_same_loop.rst +++ b/docs/source/how-to-guides/run_package_tests_in_same_loop.rst @@ -1,7 +1,7 @@ ======================================================== How to run all tests in a package in the same event loop ======================================================== -All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="package")``. +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="package")``. Add the following code to the ``__init__.py`` of the test package: .. include:: package_scoped_loop_example.py diff --git a/docs/source/how-to-guides/run_session_tests_in_same_loop.rst b/docs/source/how-to-guides/run_session_tests_in_same_loop.rst index 75bcd71e..f166fea0 100644 --- a/docs/source/how-to-guides/run_session_tests_in_same_loop.rst +++ b/docs/source/how-to-guides/run_session_tests_in_same_loop.rst @@ -1,7 +1,7 @@ ========================================================== How to run all tests in the session in the same event loop ========================================================== -All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="session")``. +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(loop_scope="session")``. The easiest way to mark all tests is via a ``pytest_collection_modifyitems`` hook in the ``conftest.py`` at the root folder of your test suite. .. include:: session_scoped_loop_example.py diff --git a/docs/source/how-to-guides/session_scoped_loop_example.py b/docs/source/how-to-guides/session_scoped_loop_example.py index 5d877116..79cc8676 100644 --- a/docs/source/how-to-guides/session_scoped_loop_example.py +++ b/docs/source/how-to-guides/session_scoped_loop_example.py @@ -5,6 +5,6 @@ def pytest_collection_modifyitems(items): pytest_asyncio_tests = (item for item in items if is_async_test(item)) - session_scope_marker = pytest.mark.asyncio(scope="session") + session_scope_marker = pytest.mark.asyncio(loop_scope="session") for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index b62e5114..9c58ae44 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +0.24.0 (UNRELEASED) +=================== +- Deprecates the optional `scope` keyword argument of asyncio markers. Users are encouraged to use the `loop_scope` keyword argument. The `loop_scope` kwarg does exactly the same, though its naming is consistent with the `loop_scope` kwarg of ``pytest_asyncio.fixture``. + + 0.23.8 (2024-07-17) =================== - Fixes a bug that caused duplicate markers in async tests `#813 `_ diff --git a/docs/source/reference/fixtures/event_loop_policy_example.py b/docs/source/reference/fixtures/event_loop_policy_example.py index cfd7ab96..5fd87b73 100644 --- a/docs/source/reference/fixtures/event_loop_policy_example.py +++ b/docs/source/reference/fixtures/event_loop_policy_example.py @@ -12,6 +12,6 @@ def event_loop_policy(request): return CustomEventLoopPolicy() -@pytest.mark.asyncio(scope="module") +@pytest.mark.asyncio(loop_scope="module") async def test_uses_custom_event_loop_policy(): assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) diff --git a/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py index 38b5689c..e75279d5 100644 --- a/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py +++ b/docs/source/reference/markers/class_scoped_loop_strict_mode_example.py @@ -3,7 +3,7 @@ import pytest -@pytest.mark.asyncio(scope="class") +@pytest.mark.asyncio(loop_scope="class") class TestClassScopedLoop: loop: asyncio.AbstractEventLoop diff --git a/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py index 538f1bd2..239f3968 100644 --- a/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py +++ b/docs/source/reference/markers/class_scoped_loop_with_fixture_strict_mode_example.py @@ -5,7 +5,7 @@ import pytest_asyncio -@pytest.mark.asyncio(scope="class") +@pytest.mark.asyncio(loop_scope="class") class TestClassScopedLoop: loop: asyncio.AbstractEventLoop diff --git a/docs/source/reference/markers/index.rst b/docs/source/reference/markers/index.rst index a875b90d..6e9be1ca 100644 --- a/docs/source/reference/markers/index.rst +++ b/docs/source/reference/markers/index.rst @@ -18,7 +18,7 @@ Multiple async tests in a single class or module can be marked using |pytestmark The ``pytest.mark.asyncio`` marker can be omitted entirely in |auto mode|_ where the *asyncio* marker is added automatically to *async* test functions. By default, each test runs in it's own asyncio event loop. -Multiple tests can share the same event loop by providing a *scope* keyword argument to the *asyncio* mark. +Multiple tests can share the same event loop by providing a *loop_scope* keyword argument to the *asyncio* mark. The supported scopes are *class,* and *module,* and *package*. The following code example provides a shared event loop for all tests in `TestClassScopedLoop`: diff --git a/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py b/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py index 221d554e..cece90db 100644 --- a/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py +++ b/docs/source/reference/markers/module_scoped_loop_strict_mode_example.py @@ -2,7 +2,7 @@ import pytest -pytestmark = pytest.mark.asyncio(scope="module") +pytestmark = pytest.mark.asyncio(loop_scope="module") loop: asyncio.AbstractEventLoop diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index e8f6d4d8..aaa1e8f3 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -996,11 +996,18 @@ def pytest_runtest_setup(item: pytest.Item) -> None: but it should only use "loop_scope". """ +_MARKER_SCOPE_KWARG_DEPRECATION_WARNING = """\ +The "scope" keyword argument to the asyncio marker has been deprecated. \ +Please use the "loop_scope" argument instead. +""" + def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName: assert asyncio_marker.name == "asyncio" - if "scope" in asyncio_marker.kwargs and "loop_scope" in asyncio_marker.kwargs: - raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR) + if "scope" in asyncio_marker.kwargs: + if "loop_scope" in asyncio_marker.kwargs: + raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR) + warnings.warn(PytestDeprecationWarning(_MARKER_SCOPE_KWARG_DEPRECATION_WARNING)) scope = asyncio_marker.kwargs.get("loop_scope") or asyncio_marker.kwargs.get( "scope", "function" ) diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py index b4d2ac94..bc6826bb 100644 --- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py +++ b/tests/async_fixtures/test_async_fixtures_with_finalizer.py @@ -4,13 +4,13 @@ import pytest -@pytest.mark.asyncio(scope="module") +@pytest.mark.asyncio(loop_scope="module") async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer): await asyncio.sleep(0.01) assert port_with_event_loop_finalizer -@pytest.mark.asyncio(scope="module") +@pytest.mark.asyncio(loop_scope="module") async def test_module_with_get_event_loop_finalizer(port_with_get_event_loop_finalizer): await asyncio.sleep(0.01) assert port_with_get_event_loop_finalizer diff --git a/tests/markers/test_class_scope.py b/tests/markers/test_class_scope.py index 3c77bab0..baac5869 100644 --- a/tests/markers/test_class_scope.py +++ b/tests/markers/test_class_scope.py @@ -39,11 +39,11 @@ def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_functions( class TestClassScopedLoop: loop: asyncio.AbstractEventLoop - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") async def test_remember_loop(self): TestClassScopedLoop.loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") async def test_this_runs_in_same_loop(self): assert asyncio.get_running_loop() is TestClassScopedLoop.loop """ @@ -62,7 +62,7 @@ def test_asyncio_mark_provides_class_scoped_loop_when_applied_to_class( import asyncio import pytest - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") class TestClassScopedLoop: loop: asyncio.AbstractEventLoop @@ -87,7 +87,7 @@ def test_asyncio_mark_raises_when_class_scoped_is_request_without_class( import asyncio import pytest - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") async def test_has_no_surrounding_class(): pass """ @@ -107,7 +107,7 @@ def test_asyncio_mark_is_inherited_to_subclasses(pytester: pytest.Pytester): import asyncio import pytest - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") class TestSuperClassWithMark: pass @@ -183,7 +183,7 @@ def test_asyncio_mark_respects_parametrized_loop_policies( def event_loop_policy(request): return request.param - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") class TestWithDifferentLoopPolicies: async def test_parametrized_loop(self, request): pass @@ -205,7 +205,7 @@ def test_asyncio_mark_provides_class_scoped_loop_to_fixtures( import pytest import pytest_asyncio - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") class TestClassScopedLoop: loop: asyncio.AbstractEventLoop @@ -242,7 +242,7 @@ async def async_fixture(self): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="function") + @pytest.mark.asyncio(loop_scope="function") async def test_runs_in_different_loop_as_fixture(self, async_fixture): global loop assert asyncio.get_running_loop() is not loop @@ -277,7 +277,7 @@ def sets_event_loop_to_none(self): return asyncio.run(asyncio.sleep(0)) # asyncio.run() sets the current event loop to None when finished - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") # parametrization may impact fixture ordering @pytest.mark.parametrize("n", (0, 1)) async def test_does_not_fail(self, sets_event_loop_to_none, n): @@ -297,7 +297,7 @@ def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_be """\ import pytest - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") class TestClass: async def test_anything(self): pass diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index 35771d93..81260006 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -69,6 +69,23 @@ async def test_raises(): result.assert_outcomes(errors=1) +def test_warns_when_scope_argument_is_present(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio(scope="function") + async def test_warns(): + ... + """ + ) + ) + result = pytester.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1, warnings=2) + result.stdout.fnmatch_lines("*DeprecationWarning*") + + def test_function_scope_supports_explicit_event_loop_fixture_request( pytester: Pytester, ): diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index 5cc6a2a7..5280ed7e 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -62,7 +62,7 @@ def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester import asyncio import pytest - pytestmark = pytest.mark.asyncio(scope="module") + pytestmark = pytest.mark.asyncio(loop_scope="module") loop: asyncio.AbstractEventLoop @@ -94,7 +94,7 @@ def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( import asyncio import pytest - pytestmark = pytest.mark.asyncio(scope="module") + pytestmark = pytest.mark.asyncio(loop_scope="module") async def test_remember_loop(event_loop): pass @@ -126,7 +126,7 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): from .custom_policy import CustomEventLoopPolicy - pytestmark = pytest.mark.asyncio(scope="module") + pytestmark = pytest.mark.asyncio(loop_scope="module") @pytest.fixture(scope="module") def event_loop_policy(): @@ -146,7 +146,7 @@ async def test_uses_custom_event_loop_policy(): from .custom_policy import CustomEventLoopPolicy - pytestmark = pytest.mark.asyncio(scope="module") + pytestmark = pytest.mark.asyncio(loop_scope="module") async def test_does_not_use_custom_event_loop_policy(): assert not isinstance( @@ -170,7 +170,7 @@ def test_asyncio_mark_respects_parametrized_loop_policies( import pytest - pytestmark = pytest.mark.asyncio(scope="module") + pytestmark = pytest.mark.asyncio(loop_scope="module") @pytest.fixture( scope="module", @@ -202,7 +202,7 @@ def test_asyncio_mark_provides_module_scoped_loop_to_fixtures( import pytest import pytest_asyncio - pytestmark = pytest.mark.asyncio(scope="module") + pytestmark = pytest.mark.asyncio(loop_scope="module") loop: asyncio.AbstractEventLoop @@ -239,7 +239,7 @@ async def async_fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") class TestMixedScopes: async def test_runs_in_different_loop_as_fixture(self, async_fixture): global loop @@ -271,7 +271,7 @@ async def async_fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="function") + @pytest.mark.asyncio(loop_scope="function") async def test_runs_in_different_loop_as_fixture(async_fixture): global loop assert asyncio.get_running_loop() is not loop @@ -301,7 +301,7 @@ async def async_fixture(): loop = asyncio.get_running_loop() yield - @pytest.mark.asyncio(scope="function") + @pytest.mark.asyncio(loop_scope="function") async def test_runs_in_different_loop_as_fixture(async_fixture): global loop assert asyncio.get_running_loop() is not loop @@ -334,7 +334,7 @@ def sets_event_loop_to_none(): return asyncio.run(asyncio.sleep(0)) # asyncio.run() sets the current event loop to None when finished - @pytest.mark.asyncio(scope="module") + @pytest.mark.asyncio(loop_scope="module") # parametrization may impact fixture ordering @pytest.mark.parametrize("n", (0, 1)) async def test_does_not_fail(sets_event_loop_to_none, n): @@ -354,7 +354,7 @@ def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_be """\ import pytest - @pytest.mark.asyncio(scope="module") + @pytest.mark.asyncio(loop_scope="module") async def test_anything(): pass """ diff --git a/tests/markers/test_package_scope.py b/tests/markers/test_package_scope.py index c80289be..849967e8 100644 --- a/tests/markers/test_package_scope.py +++ b/tests/markers/test_package_scope.py @@ -22,7 +22,7 @@ def test_asyncio_mark_provides_package_scoped_loop_strict_mode(pytester: Pyteste from {package_name} import shared_module - @pytest.mark.asyncio(scope="package") + @pytest.mark.asyncio(loop_scope="package") async def test_remember_loop(): shared_module.loop = asyncio.get_running_loop() """ @@ -34,7 +34,7 @@ async def test_remember_loop(): from {package_name} import shared_module - pytestmark = pytest.mark.asyncio(scope="package") + pytestmark = pytest.mark.asyncio(loop_scope="package") async def test_this_runs_in_same_loop(): assert asyncio.get_running_loop() is shared_module.loop @@ -55,7 +55,7 @@ async def test_this_runs_in_same_loop(self): from {package_name} import shared_module - pytestmark = pytest.mark.asyncio(scope="package") + pytestmark = pytest.mark.asyncio(loop_scope="package") async def test_subpackage_runs_in_different_loop(): assert asyncio.get_running_loop() is not shared_module.loop @@ -76,7 +76,7 @@ def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( import asyncio import pytest - @pytest.mark.asyncio(scope="package") + @pytest.mark.asyncio(loop_scope="package") async def test_remember_loop(event_loop): pass """ @@ -118,7 +118,7 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): from .custom_policy import CustomEventLoopPolicy - pytestmark = pytest.mark.asyncio(scope="package") + pytestmark = pytest.mark.asyncio(loop_scope="package") async def test_uses_custom_event_loop_policy(): assert isinstance( @@ -134,7 +134,7 @@ async def test_uses_custom_event_loop_policy(): from .custom_policy import CustomEventLoopPolicy - pytestmark = pytest.mark.asyncio(scope="package") + pytestmark = pytest.mark.asyncio(loop_scope="package") async def test_also_uses_custom_event_loop_policy(): assert isinstance( @@ -159,7 +159,7 @@ def test_asyncio_mark_respects_parametrized_loop_policies( import pytest - pytestmark = pytest.mark.asyncio(scope="package") + pytestmark = pytest.mark.asyncio(loop_scope="package") @pytest.fixture( scope="package", @@ -215,7 +215,7 @@ async def my_fixture(): from {package_name} import shared_module - pytestmark = pytest.mark.asyncio(scope="package") + pytestmark = pytest.mark.asyncio(loop_scope="package") async def test_runs_in_same_loop_as_fixture(my_fixture): assert asyncio.get_running_loop() is shared_module.loop @@ -245,7 +245,7 @@ async def async_fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="module") + @pytest.mark.asyncio(loop_scope="module") async def test_runs_in_different_loop_as_fixture(async_fixture): global loop assert asyncio.get_running_loop() is not loop @@ -275,7 +275,7 @@ async def async_fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") class TestMixedScopes: async def test_runs_in_different_loop_as_fixture(self, async_fixture): global loop @@ -340,7 +340,7 @@ def sets_event_loop_to_none(): return asyncio.run(asyncio.sleep(0)) # asyncio.run() sets the current event loop to None when finished - @pytest.mark.asyncio(scope="package") + @pytest.mark.asyncio(loop_scope="package") # parametrization may impact fixture ordering @pytest.mark.parametrize("n", (0, 1)) async def test_does_not_fail(sets_event_loop_to_none, n): @@ -361,7 +361,7 @@ def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_be """\ import pytest - @pytest.mark.asyncio(scope="package") + @pytest.mark.asyncio(loop_scope="package") async def test_anything(): pass """ diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py index b8b747a0..7900ef48 100644 --- a/tests/markers/test_session_scope.py +++ b/tests/markers/test_session_scope.py @@ -21,7 +21,7 @@ def test_asyncio_mark_provides_session_scoped_loop_strict_mode(pytester: Pyteste from {package_name} import shared_module - @pytest.mark.asyncio(scope="session") + @pytest.mark.asyncio(loop_scope="session") async def test_remember_loop(): shared_module.loop = asyncio.get_running_loop() """ @@ -33,7 +33,7 @@ async def test_remember_loop(): from {package_name} import shared_module - pytestmark = pytest.mark.asyncio(scope="session") + pytestmark = pytest.mark.asyncio(loop_scope="session") async def test_this_runs_in_same_loop(): assert asyncio.get_running_loop() is shared_module.loop @@ -56,7 +56,7 @@ async def test_this_runs_in_same_loop(self): from {package_name} import shared_module - pytestmark = pytest.mark.asyncio(scope="session") + pytestmark = pytest.mark.asyncio(loop_scope="session") async def test_subpackage_runs_in_same_loop(): assert asyncio.get_running_loop() is shared_module.loop @@ -77,7 +77,7 @@ def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( import asyncio import pytest - @pytest.mark.asyncio(scope="session") + @pytest.mark.asyncio(loop_scope="session") async def test_remember_loop(event_loop): pass """ @@ -119,7 +119,7 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): from .custom_policy import CustomEventLoopPolicy - pytestmark = pytest.mark.asyncio(scope="session") + pytestmark = pytest.mark.asyncio(loop_scope="session") async def test_uses_custom_event_loop_policy(): assert isinstance( @@ -135,7 +135,7 @@ async def test_uses_custom_event_loop_policy(): from .custom_policy import CustomEventLoopPolicy - pytestmark = pytest.mark.asyncio(scope="session") + pytestmark = pytest.mark.asyncio(loop_scope="session") async def test_also_uses_custom_event_loop_policy(): assert isinstance( @@ -160,7 +160,7 @@ def test_asyncio_mark_respects_parametrized_loop_policies( import pytest - pytestmark = pytest.mark.asyncio(scope="session") + pytestmark = pytest.mark.asyncio(loop_scope="session") @pytest.fixture( scope="session", @@ -220,7 +220,7 @@ async def my_fixture(): from {package_name} import shared_module - pytestmark = pytest.mark.asyncio(scope="session") + pytestmark = pytest.mark.asyncio(loop_scope="session") async def test_runs_in_same_loop_as_fixture(my_fixture): assert asyncio.get_running_loop() is shared_module.loop @@ -250,7 +250,7 @@ async def async_fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="package") + @pytest.mark.asyncio(loop_scope="package") async def test_runs_in_different_loop_as_fixture(async_fixture): global loop assert asyncio.get_running_loop() is not loop @@ -280,7 +280,7 @@ async def async_fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="module") + @pytest.mark.asyncio(loop_scope="module") async def test_runs_in_different_loop_as_fixture(async_fixture): global loop assert asyncio.get_running_loop() is not loop @@ -310,7 +310,7 @@ async def async_fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") class TestMixedScopes: async def test_runs_in_different_loop_as_fixture(self, async_fixture): global loop @@ -405,7 +405,7 @@ def sets_event_loop_to_none(): return asyncio.run(asyncio.sleep(0)) # asyncio.run() sets the current event loop to None when finished - @pytest.mark.asyncio(scope="session") + @pytest.mark.asyncio(loop_scope="session") # parametrization may impact fixture ordering @pytest.mark.parametrize("n", (0, 1)) async def test_does_not_fail(sets_event_loop_to_none, n): @@ -425,7 +425,7 @@ def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_be """\ import pytest - @pytest.mark.asyncio(scope="session") + @pytest.mark.asyncio(loop_scope="session") async def test_anything(): pass """ diff --git a/tests/test_fixture_loop_scopes.py b/tests/test_fixture_loop_scopes.py index 737a5573..f0271e59 100644 --- a/tests/test_fixture_loop_scopes.py +++ b/tests/test_fixture_loop_scopes.py @@ -25,7 +25,7 @@ async def fixture(): global loop loop = asyncio.get_running_loop() - @pytest.mark.asyncio(scope="session") + @pytest.mark.asyncio(loop_scope="session") async def test_runs_in_same_loop_as_fixture(fixture): global loop assert loop == asyncio.get_running_loop() @@ -60,7 +60,7 @@ def test_default_loop_scope_config_option_changes_fixture_loop_scope( async def fixture_loop(): return asyncio.get_running_loop() - @pytest.mark.asyncio(scope="{default_loop_scope}") + @pytest.mark.asyncio(loop_scope="{default_loop_scope}") async def test_runs_in_fixture_loop(fixture_loop): assert asyncio.get_running_loop() is fixture_loop """ @@ -93,7 +93,7 @@ class TestClass: async def fixture_loop(self): return asyncio.get_running_loop() - @pytest.mark.asyncio(scope="class") + @pytest.mark.asyncio(loop_scope="class") async def test_runs_in_fixture_loop(self, fixture_loop): assert asyncio.get_running_loop() is fixture_loop """ @@ -126,7 +126,7 @@ def test_default_package_loop_scope_config_option_changes_fixture_loop_scope( async def fixture_loop(): return asyncio.get_running_loop() - @pytest.mark.asyncio(scope="package") + @pytest.mark.asyncio(loop_scope="package") async def test_runs_in_fixture_loop(fixture_loop): assert asyncio.get_running_loop() is fixture_loop """ From 111d452a31b42d7a83c524699aab861ebad20ebd Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 25 Jul 2024 08:18:26 +0200 Subject: [PATCH 08/11] docs: Added how-to guide on changing the event loop scope of a fixture. Signed-off-by: Michael Seifert --- docs/source/how-to-guides/change_fixture_loop.rst | 7 +++++++ .../how-to-guides/change_fixture_loop_example.py | 15 +++++++++++++++ docs/source/how-to-guides/index.rst | 1 + docs/source/reference/decorators/index.rst | 2 ++ 4 files changed, 25 insertions(+) create mode 100644 docs/source/how-to-guides/change_fixture_loop.rst create mode 100644 docs/source/how-to-guides/change_fixture_loop_example.py diff --git a/docs/source/how-to-guides/change_fixture_loop.rst b/docs/source/how-to-guides/change_fixture_loop.rst new file mode 100644 index 00000000..c6c8b8e6 --- /dev/null +++ b/docs/source/how-to-guides/change_fixture_loop.rst @@ -0,0 +1,7 @@ +=============================================== +How to change the event loop scope of a fixture +=============================================== +The event loop scope of an asynchronous fixture is specified via the *loop_scope* keyword argument to :ref:`pytest_asyncio.fixture `. The following fixture runs in the module-scoped event loop: + +.. include:: change_fixture_loop_example.py + :code: python diff --git a/docs/source/how-to-guides/change_fixture_loop_example.py b/docs/source/how-to-guides/change_fixture_loop_example.py new file mode 100644 index 00000000..dc6d2ef3 --- /dev/null +++ b/docs/source/how-to-guides/change_fixture_loop_example.py @@ -0,0 +1,15 @@ +import asyncio + +import pytest + +import pytest_asyncio + + +@pytest_asyncio.fixture(loop_scope="module") +async def current_loop(): + return asyncio.get_running_loop() + + +@pytest.mark.asyncio(loop_scope="module") +async def test_runs_in_module_loop(current_loop): + assert current_loop is asyncio.get_running_loop() diff --git a/docs/source/how-to-guides/index.rst b/docs/source/how-to-guides/index.rst index a61ead50..e47561d2 100644 --- a/docs/source/how-to-guides/index.rst +++ b/docs/source/how-to-guides/index.rst @@ -5,6 +5,7 @@ How-To Guides .. toctree:: :hidden: + change_fixture_loop run_class_tests_in_same_loop run_module_tests_in_same_loop run_package_tests_in_same_loop diff --git a/docs/source/reference/decorators/index.rst b/docs/source/reference/decorators/index.rst index 5c96cf4b..c049c5dd 100644 --- a/docs/source/reference/decorators/index.rst +++ b/docs/source/reference/decorators/index.rst @@ -1,3 +1,5 @@ +.. _decorators/pytest_asyncio_fixture: + ========== Decorators ========== From a89b0fe01ed38f60423f144f79dbd91aa8195d1b Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 25 Jul 2024 08:40:14 +0200 Subject: [PATCH 09/11] docs: Added how-to guide on changing the default event loop scope of all fixtures. Signed-off-by: Michael Seifert --- .../change_default_fixture_loop.rst | 24 +++++++++++++++++++ docs/source/how-to-guides/index.rst | 1 + docs/source/reference/configuration.rst | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 docs/source/how-to-guides/change_default_fixture_loop.rst diff --git a/docs/source/how-to-guides/change_default_fixture_loop.rst b/docs/source/how-to-guides/change_default_fixture_loop.rst new file mode 100644 index 00000000..b54fef8e --- /dev/null +++ b/docs/source/how-to-guides/change_default_fixture_loop.rst @@ -0,0 +1,24 @@ +========================================================== +How to change the default event loop scope of all fixtures +========================================================== +The :ref:`configuration/asyncio_default_fixture_loop_scope` configuration option sets the default event loop scope for asynchronous fixtures. The following code snippets configure all fixtures to run in a session-scoped loop by default: + +.. code-block:: ini + :caption: pytest.ini + + [pytest] + asyncio_default_fixture_loop_scope = session + +.. code-block:: toml + :caption: pyproject.toml + + [tool.pytest.ini_options] + asyncio_default_fixture_loop_scope = "session" + +.. code-block:: ini + :caption: setup.cfg + + [tool:pytest] + asyncio_default_fixture_loop_scope = session + +Please refer to :ref:`configuration/asyncio_default_fixture_loop_scope` for other valid scopes. diff --git a/docs/source/how-to-guides/index.rst b/docs/source/how-to-guides/index.rst index e47561d2..4f03dd35 100644 --- a/docs/source/how-to-guides/index.rst +++ b/docs/source/how-to-guides/index.rst @@ -6,6 +6,7 @@ How-To Guides :hidden: change_fixture_loop + change_default_fixture_loop run_class_tests_in_same_loop run_module_tests_in_same_loop run_package_tests_in_same_loop diff --git a/docs/source/reference/configuration.rst b/docs/source/reference/configuration.rst index 7b0d1f95..35c67302 100644 --- a/docs/source/reference/configuration.rst +++ b/docs/source/reference/configuration.rst @@ -2,6 +2,8 @@ Configuration ============= +.. _configuration/asyncio_default_fixture_loop_scope: + asyncio_default_fixture_loop_scope ================================== Determines the default event loop scope of asynchronous fixtures. When this configuration option is unset, it defaults to the fixture scope. In future versions of pytest-asyncio, the value will default to ``function`` when unset. Possible values are: ``function``, ``class``, ``module``, ``package``, ``session`` From a4d64c8f6b56ac82610dc2f10447e7498257aed6 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 25 Jul 2024 09:23:48 +0200 Subject: [PATCH 10/11] docs: Updated documentation on decorators to address the addition of the loop_scope keyword argument. Signed-off-by: Michael Seifert --- docs/source/concepts.rst | 2 ++ .../decorators/fixture_strict_mode_example.py | 14 ----------- docs/source/reference/decorators/index.rst | 23 +++++++++++-------- .../pytest_asyncio_fixture_example.py | 17 ++++++++++++++ 4 files changed, 33 insertions(+), 23 deletions(-) delete mode 100644 docs/source/reference/decorators/fixture_strict_mode_example.py create mode 100644 docs/source/reference/decorators/pytest_asyncio_fixture_example.py diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst index 298d08f2..be8b775b 100644 --- a/docs/source/concepts.rst +++ b/docs/source/concepts.rst @@ -2,6 +2,8 @@ Concepts ======== +.. _concepts/event_loops: + asyncio event loops =================== In order to understand how pytest-asyncio works, it helps to understand how pytest collectors work. diff --git a/docs/source/reference/decorators/fixture_strict_mode_example.py b/docs/source/reference/decorators/fixture_strict_mode_example.py deleted file mode 100644 index 6442c103..00000000 --- a/docs/source/reference/decorators/fixture_strict_mode_example.py +++ /dev/null @@ -1,14 +0,0 @@ -import asyncio - -import pytest_asyncio - - -@pytest_asyncio.fixture -async def async_gen_fixture(): - await asyncio.sleep(0.1) - yield "a value" - - -@pytest_asyncio.fixture(scope="module") -async def async_fixture(): - return await asyncio.sleep(0.1) diff --git a/docs/source/reference/decorators/index.rst b/docs/source/reference/decorators/index.rst index c049c5dd..0fcb7087 100644 --- a/docs/source/reference/decorators/index.rst +++ b/docs/source/reference/decorators/index.rst @@ -3,15 +3,20 @@ ========== Decorators ========== -Asynchronous fixtures are defined just like ordinary pytest fixtures, except they should be decorated with ``@pytest_asyncio.fixture``. +The ``@pytest_asyncio.fixture`` decorator allows coroutines and async generator functions to be used as pytest fixtures. -.. include:: fixture_strict_mode_example.py - :code: python +The decorator takes all arguments supported by `@pytest.fixture`. +Additionally, ``@pytest_asyncio.fixture`` supports the *loop_scope* keyword argument, which selects the event loop in which the fixture is run (see :ref:`concepts/event_loops`). +The default event loop scope is *function* scope. +Possible loop scopes are *session,* *package,* *module,* *class,* and *function*. + +The *loop_scope* of a fixture can be chosen independently from its caching *scope*. +However, the event loop scope must be larger or the same as the fixture's caching scope. +In other words, it's possible to reevaluate an async fixture multiple times within the same event loop, but it's not possible to switch out the running event loop in an async fixture. -All scopes are supported, but if you use a non-function scope you will need -to redefine the ``event_loop`` fixture to have the same or broader scope. -Async fixtures need the event loop, and so must have the same or narrower scope -than the ``event_loop`` fixture. +Examples: + +.. include:: pytest_asyncio_fixture_example.py + :code: python -*auto* mode automatically converts async fixtures declared with the -standard ``@pytest.fixture`` decorator to *asyncio-driven* versions. +*auto* mode automatically converts coroutines and async generator functions declared with the standard ``@pytest.fixture`` decorator to pytest-asyncio fixtures. diff --git a/docs/source/reference/decorators/pytest_asyncio_fixture_example.py b/docs/source/reference/decorators/pytest_asyncio_fixture_example.py new file mode 100644 index 00000000..3123f11d --- /dev/null +++ b/docs/source/reference/decorators/pytest_asyncio_fixture_example.py @@ -0,0 +1,17 @@ +import pytest_asyncio + + +@pytest_asyncio.fixture +async def fixture_runs_in_fresh_loop_for_every_function(): ... + + +@pytest_asyncio.fixture(loop_scope="session", scope="module") +async def fixture_runs_in_session_loop_once_per_module(): ... + + +@pytest_asyncio.fixture(loop_scope="module", scope="module") +async def fixture_runs_in_module_loop_once_per_module(): ... + + +@pytest_asyncio.fixture(loop_scope="module") +async def fixture_runs_in_module_loop_once_per_function(): ... From 5f34edf6d95bb01be2a7f6869c26da3527299c7e Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Thu, 25 Jul 2024 09:45:56 +0200 Subject: [PATCH 11/11] docs: Added missing changelog entry about the loop_scope kwarg. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 9c58ae44..82987a05 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -4,7 +4,8 @@ Changelog 0.24.0 (UNRELEASED) =================== -- Deprecates the optional `scope` keyword argument of asyncio markers. Users are encouraged to use the `loop_scope` keyword argument. The `loop_scope` kwarg does exactly the same, though its naming is consistent with the `loop_scope` kwarg of ``pytest_asyncio.fixture``. +- Adds an optional `loop_scope` keyword argument to `pytest.mark.asyncio`. This argument controls which event loop is used to run the marked async test. `#706`_, `#871 `_ +- Deprecates the optional `scope` keyword argument to `pytest.mark.asyncio` for API consistency with ``pytest_asyncio.fixture``. Users are encouraged to use the `loop_scope` keyword argument, which does exactly the same. 0.23.8 (2024-07-17)