Skip to content

Commit cb06135

Browse files
committed
feat: Configuration option for setting default loop_scope for tests
New configuration option, asyncio_default_test_loop_scope, provides default value for loop_scope argument of asyncio marker. This can be used to use the same event loop in auto mode without need to use modifyitems hook. Test functions can still override loop_scope by using asyncio marker.
1 parent 623ab74 commit cb06135

File tree

5 files changed

+136
-6
lines changed

5 files changed

+136
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
=======================================================
2+
How to change the default event loop scope of all tests
3+
=======================================================
4+
The :ref:`configuration/asyncio_default_test_loop_scope` configuration option sets the default event loop scope for asynchronous tests. The following code snippets configure all tests to run in a session-scoped loop by default:
5+
6+
.. code-block:: ini
7+
:caption: pytest.ini
8+
9+
[pytest]
10+
asyncio_default_test_loop_scope = session
11+
12+
.. code-block:: toml
13+
:caption: pyproject.toml
14+
15+
[tool.pytest.ini_options]
16+
asyncio_default_test_loop_scope = "session"
17+
18+
.. code-block:: ini
19+
:caption: setup.cfg
20+
21+
[tool:pytest]
22+
asyncio_default_test_loop_scope = session
23+
24+
Please refer to :ref:`configuration/asyncio_default_test_loop_scope` for other valid scopes.

docs/how-to-guides/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ How-To Guides
99
migrate_from_0_23
1010
change_fixture_loop
1111
change_default_fixture_loop
12+
change_default_test_loop
1213
run_class_tests_in_same_loop
1314
run_module_tests_in_same_loop
1415
run_package_tests_in_same_loop

docs/reference/configuration.rst

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ asyncio_default_fixture_loop_scope
88
==================================
99
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``
1010

11+
.. _configuration/asyncio_default_test_loop_scope:
12+
13+
asyncio_default_test_loop_scope
14+
===============================
15+
Determines the default event loop scope of asynchronous tests. When this configuration option is unset, it default to function scope. Possible values are: ``function``, ``class``, ``module``, ``package``, ``session``
16+
1117
asyncio_mode
1218
============
1319
The pytest-asyncio mode can be set by the ``asyncio_mode`` configuration option in the `configuration file

pytest_asyncio/plugin.py

+28-6
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None
107107
help="default scope of the asyncio event loop used to execute async fixtures",
108108
default=None,
109109
)
110+
parser.addini(
111+
"asyncio_default_test_loop_scope",
112+
type="string",
113+
help="default scope of the asyncio event loop used to execute tests",
114+
default="function",
115+
)
110116

111117

112118
@overload
@@ -217,9 +223,15 @@ def pytest_configure(config: Config) -> None:
217223
def pytest_report_header(config: Config) -> list[str]:
218224
"""Add asyncio config to pytest header."""
219225
mode = _get_asyncio_mode(config)
220-
default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
226+
default_fixture_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
227+
default_test_loop_scope = _get_default_test_loop_scope(config)
228+
header = [
229+
f'mode={mode}',
230+
f'asyncio_default_fixture_loop_scope={default_fixture_loop_scope}',
231+
f'asyncio_default_test_loop_scope={default_test_loop_scope}',
232+
]
221233
return [
222-
f"asyncio: mode={mode}, asyncio_default_fixture_loop_scope={default_loop_scope}"
234+
'asyncio: ' + ', '.join(header),
223235
]
224236

225237

@@ -806,7 +818,8 @@ def pytest_generate_tests(metafunc: Metafunc) -> None:
806818
marker = metafunc.definition.get_closest_marker("asyncio")
807819
if not marker:
808820
return
809-
scope = _get_marked_loop_scope(marker)
821+
default_loop_scope = _get_default_test_loop_scope(metafunc.config)
822+
scope = _get_marked_loop_scope(marker, default_loop_scope)
810823
if scope == "function":
811824
return
812825
event_loop_node = _retrieve_scope_root(metafunc.definition, scope)
@@ -1077,7 +1090,8 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
10771090
marker = item.get_closest_marker("asyncio")
10781091
if marker is None:
10791092
return
1080-
scope = _get_marked_loop_scope(marker)
1093+
default_loop_scope = _get_default_test_loop_scope(item.config)
1094+
scope = _get_marked_loop_scope(marker, default_loop_scope)
10811095
if scope != "function":
10821096
parent_node = _retrieve_scope_root(item, scope)
10831097
event_loop_fixture_id = parent_node.stash[_event_loop_fixture_id]
@@ -1107,7 +1121,9 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
11071121
"""
11081122

11091123

1110-
def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName:
1124+
def _get_marked_loop_scope(
1125+
asyncio_marker: Mark, default_loop_scope: _ScopeName
1126+
) -> _ScopeName:
11111127
assert asyncio_marker.name == "asyncio"
11121128
if asyncio_marker.args or (
11131129
asyncio_marker.kwargs and set(asyncio_marker.kwargs) - {"loop_scope", "scope"}
@@ -1118,12 +1134,18 @@ def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName:
11181134
raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR)
11191135
warnings.warn(PytestDeprecationWarning(_MARKER_SCOPE_KWARG_DEPRECATION_WARNING))
11201136
scope = asyncio_marker.kwargs.get("loop_scope") or asyncio_marker.kwargs.get(
1121-
"scope", "function"
1137+
"scope"
11221138
)
1139+
if scope is None:
1140+
scope = default_loop_scope
11231141
assert scope in {"function", "class", "module", "package", "session"}
11241142
return scope
11251143

11261144

1145+
def _get_default_test_loop_scope(config: Config) -> _ScopeName:
1146+
return config.getini("asyncio_default_test_loop_scope")
1147+
1148+
11271149
def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector:
11281150
node_type_by_scope = {
11291151
"class": Class,

tests/test_asyncio_mark.py

+77
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,80 @@ async def test_a():
146146
result.stdout.fnmatch_lines(
147147
["*Tests based on asynchronous generators are not supported*"]
148148
)
149+
150+
151+
def test_asyncio_marker_fallbacks_to_configured_default_loop_scope_if_not_set(
152+
pytester: Pytester,
153+
):
154+
pytester.makeini(
155+
dedent(
156+
"""\
157+
[pytest]
158+
asyncio_default_fixture_loop_scope = function
159+
asyncio_default_test_loop_scope = session
160+
"""
161+
)
162+
)
163+
164+
pytester.makepyfile(
165+
dedent(
166+
"""\
167+
import asyncio
168+
import pytest_asyncio
169+
import pytest
170+
171+
loop: asyncio.AbstractEventLoop
172+
173+
@pytest_asyncio.fixture(loop_scope="session", scope="session")
174+
async def session_loop_fixture():
175+
global loop
176+
loop = asyncio.get_running_loop()
177+
178+
async def test_a(session_loop_fixture):
179+
global loop
180+
assert asyncio.get_running_loop() is loop
181+
"""
182+
)
183+
)
184+
185+
result = pytester.runpytest("--asyncio-mode=auto")
186+
result.assert_outcomes(passed=1)
187+
188+
189+
def test_asyncio_marker_uses_marker_loop_scope_even_if_config_is_set(
190+
pytester: Pytester,
191+
):
192+
pytester.makeini(
193+
dedent(
194+
"""\
195+
[pytest]
196+
asyncio_default_fixture_loop_scope = function
197+
asyncio_default_test_loop_scope = module
198+
"""
199+
)
200+
)
201+
202+
pytester.makepyfile(
203+
dedent(
204+
"""\
205+
import asyncio
206+
import pytest_asyncio
207+
import pytest
208+
209+
loop: asyncio.AbstractEventLoop
210+
211+
@pytest_asyncio.fixture(loop_scope="session", scope="session")
212+
async def session_loop_fixture():
213+
global loop
214+
loop = asyncio.get_running_loop()
215+
216+
@pytest.mark.asyncio(loop_scope="session")
217+
async def test_a(session_loop_fixture):
218+
global loop
219+
assert asyncio.get_running_loop() is loop
220+
"""
221+
)
222+
)
223+
224+
result = pytester.runpytest("--asyncio-mode=auto")
225+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)