Skip to content

Commit af50ce6

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 af50ce6

File tree

5 files changed

+129
-8
lines changed

5 files changed

+129
-8
lines changed
Lines changed: 24 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 21 additions & 8 deletions
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,10 @@ 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)
221228
return [
222-
f"asyncio: mode={mode}, asyncio_default_fixture_loop_scope={default_loop_scope}"
229+
f"asyncio: mode={mode}, asyncio_default_fixture_loop_scope={default_fixture_loop_scope}, asyncio_default_test_loop_scope={default_test_loop_scope}"
223230
]
224231

225232

@@ -806,7 +813,8 @@ def pytest_generate_tests(metafunc: Metafunc) -> None:
806813
marker = metafunc.definition.get_closest_marker("asyncio")
807814
if not marker:
808815
return
809-
scope = _get_marked_loop_scope(marker)
816+
default_loop_scope = _get_default_test_loop_scope(metafunc.config)
817+
scope = _get_marked_loop_scope(marker, default_loop_scope)
810818
if scope == "function":
811819
return
812820
event_loop_node = _retrieve_scope_root(metafunc.definition, scope)
@@ -1077,7 +1085,8 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
10771085
marker = item.get_closest_marker("asyncio")
10781086
if marker is None:
10791087
return
1080-
scope = _get_marked_loop_scope(marker)
1088+
default_loop_scope = _get_default_test_loop_scope(item.config)
1089+
scope = _get_marked_loop_scope(marker, default_loop_scope)
10811090
if scope != "function":
10821091
parent_node = _retrieve_scope_root(item, scope)
10831092
event_loop_fixture_id = parent_node.stash[_event_loop_fixture_id]
@@ -1107,7 +1116,7 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
11071116
"""
11081117

11091118

1110-
def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName:
1119+
def _get_marked_loop_scope(asyncio_marker: Mark, default_loop_scope: _ScopeName) -> _ScopeName:
11111120
assert asyncio_marker.name == "asyncio"
11121121
if asyncio_marker.args or (
11131122
asyncio_marker.kwargs and set(asyncio_marker.kwargs) - {"loop_scope", "scope"}
@@ -1117,13 +1126,17 @@ def _get_marked_loop_scope(asyncio_marker: Mark) -> _ScopeName:
11171126
if "loop_scope" in asyncio_marker.kwargs:
11181127
raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR)
11191128
warnings.warn(PytestDeprecationWarning(_MARKER_SCOPE_KWARG_DEPRECATION_WARNING))
1120-
scope = asyncio_marker.kwargs.get("loop_scope") or asyncio_marker.kwargs.get(
1121-
"scope", "function"
1122-
)
1129+
scope = asyncio_marker.kwargs.get("loop_scope") or asyncio_marker.kwargs.get("scope")
1130+
if scope is None:
1131+
scope = default_loop_scope
11231132
assert scope in {"function", "class", "module", "package", "session"}
11241133
return scope
11251134

11261135

1136+
def _get_default_test_loop_scope(config: Config) -> _ScopeName | None:
1137+
return config.getini("asyncio_default_test_loop_scope")
1138+
1139+
11271140
def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector:
11281141
node_type_by_scope = {
11291142
"class": Class,

tests/test_asyncio_mark.py

Lines changed: 77 additions & 0 deletions
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)