Skip to content

Commit 9d4ad54

Browse files
committed
[feat] Added the asyncio_default_fixture_loop_scope configuration option.
Signed-off-by: Michael Seifert <[email protected]>
1 parent 8144a03 commit 9d4ad54

File tree

3 files changed

+133
-3
lines changed

3 files changed

+133
-3
lines changed

docs/source/reference/configuration.rst

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Configuration
33
=============
44

5+
asyncio_default_fixture_loop_scope
6+
==================================
7+
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``
8+
9+
asyncio_mode
10+
============
511
The pytest-asyncio mode can be set by the ``asyncio_mode`` configuration option in the `configuration file
612
<https://docs.pytest.org/en/latest/reference/customize.html>`_:
713

pytest_asyncio/plugin.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None
103103
help="default value for --asyncio-mode",
104104
default="strict",
105105
)
106+
parser.addini(
107+
"asyncio_default_fixture_loop_scope",
108+
type="string",
109+
help="default scope of the asyncio event loop used to execute async fixtures",
110+
default=None,
111+
)
106112

107113

108114
@overload
@@ -189,8 +195,20 @@ def _get_asyncio_mode(config: Config) -> Mode:
189195
)
190196

191197

198+
_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET = """\
199+
The configuration option "asyncio_default_fixture_loop_scope" is unset.
200+
The event loop scope for asynchronous fixtures will default to the fixture caching \
201+
scope. Future versions of pytest-asyncio will default the loop scope for asynchronous \
202+
fixtures to function scope. Set the default fixture loop scope explicitly in order to \
203+
avoid unexpected behavior in the future. Valid fixture loop scopes are: \
204+
"function", "class", "module", "package", "session"
205+
"""
206+
207+
192208
def pytest_configure(config: Config) -> None:
193-
"""Inject documentation."""
209+
default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
210+
if not default_loop_scope:
211+
warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))
194212
config.addinivalue_line(
195213
"markers",
196214
"asyncio: "
@@ -203,14 +221,16 @@ def pytest_configure(config: Config) -> None:
203221
def pytest_report_header(config: Config) -> List[str]:
204222
"""Add asyncio config to pytest header."""
205223
mode = _get_asyncio_mode(config)
206-
return [f"asyncio: mode={mode}"]
224+
default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
225+
return [f"asyncio: mode={mode}, default_loop_scope={default_loop_scope}"]
207226

208227

209228
def _preprocess_async_fixtures(
210229
collector: Collector,
211230
processed_fixturedefs: Set[FixtureDef],
212231
) -> None:
213232
config = collector.config
233+
default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
214234
asyncio_mode = _get_asyncio_mode(config)
215235
fixturemanager = config.pluginmanager.get_plugin("funcmanage")
216236
assert fixturemanager is not None
@@ -225,7 +245,11 @@ def _preprocess_async_fixtures(
225245
# Ignore async fixtures without explicit asyncio mark in strict mode
226246
# This applies to pytest_trio fixtures, for example
227247
continue
228-
scope = getattr(func, "_loop_scope", None) or fixturedef.scope
248+
scope = (
249+
getattr(func, "_loop_scope", None)
250+
or default_loop_scope
251+
or fixturedef.scope
252+
)
229253
if scope == "function":
230254
event_loop_fixture_id: Optional[str] = "event_loop"
231255
else:

tests/test_fixture_loop_scopes.py

+100
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,103 @@ async def test_runs_in_same_loop_as_fixture(fixture):
3434
)
3535
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
3636
result.assert_outcomes(passed=1)
37+
38+
39+
@pytest.mark.parametrize("default_loop_scope", ("function", "module", "session"))
40+
def test_default_loop_scope_config_option_changes_fixture_loop_scope(
41+
pytester: Pytester,
42+
default_loop_scope: str,
43+
):
44+
pytester.makeini(
45+
dedent(
46+
f"""\
47+
[pytest]
48+
asyncio_default_fixture_loop_scope = {default_loop_scope}
49+
"""
50+
)
51+
)
52+
pytester.makepyfile(
53+
dedent(
54+
f"""\
55+
import asyncio
56+
import pytest
57+
import pytest_asyncio
58+
59+
@pytest_asyncio.fixture
60+
async def fixture_loop():
61+
return asyncio.get_running_loop()
62+
63+
@pytest.mark.asyncio(scope="{default_loop_scope}")
64+
async def test_runs_in_fixture_loop(fixture_loop):
65+
assert asyncio.get_running_loop() is fixture_loop
66+
"""
67+
)
68+
)
69+
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
70+
result.assert_outcomes(passed=1)
71+
72+
73+
def test_default_class_loop_scope_config_option_changes_fixture_loop_scope(
74+
pytester: Pytester,
75+
):
76+
pytester.makeini(
77+
dedent(
78+
"""\
79+
[pytest]
80+
asyncio_default_fixture_loop_scope = class
81+
"""
82+
)
83+
)
84+
pytester.makepyfile(
85+
dedent(
86+
"""\
87+
import asyncio
88+
import pytest
89+
import pytest_asyncio
90+
91+
class TestClass:
92+
@pytest_asyncio.fixture
93+
async def fixture_loop(self):
94+
return asyncio.get_running_loop()
95+
96+
@pytest.mark.asyncio(scope="class")
97+
async def test_runs_in_fixture_loop(self, fixture_loop):
98+
assert asyncio.get_running_loop() is fixture_loop
99+
"""
100+
)
101+
)
102+
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
103+
result.assert_outcomes(passed=1)
104+
105+
106+
def test_default_package_loop_scope_config_option_changes_fixture_loop_scope(
107+
pytester: Pytester,
108+
):
109+
pytester.makeini(
110+
dedent(
111+
"""\
112+
[pytest]
113+
asyncio_default_fixture_loop_scope = package
114+
"""
115+
)
116+
)
117+
pytester.makepyfile(
118+
__init__="",
119+
test_a=dedent(
120+
"""\
121+
import asyncio
122+
import pytest
123+
import pytest_asyncio
124+
125+
@pytest_asyncio.fixture
126+
async def fixture_loop():
127+
return asyncio.get_running_loop()
128+
129+
@pytest.mark.asyncio(scope="package")
130+
async def test_runs_in_fixture_loop(fixture_loop):
131+
assert asyncio.get_running_loop() is fixture_loop
132+
"""
133+
),
134+
)
135+
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
136+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)