Skip to content

Commit c1ec0f0

Browse files
committed
[feat] The asyncio_event_loop mark provides a module-scoped asyncio event loop when a module has the mark.
Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de>
1 parent be36ce6 commit c1ec0f0

File tree

3 files changed

+98
-6
lines changed

3 files changed

+98
-6
lines changed

docs/source/reference/markers.rst

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ automatically to *async* test functions.
3232

3333
``pytest.mark.asyncio_event_loop``
3434
==================================
35-
Test classes with this mark provide a class-scoped asyncio event loop.
35+
Test classes or modules with this mark provide a class-scoped or module-scoped asyncio event loop.
3636

3737
This functionality is orthogonal to the `asyncio` mark.
38-
That means the presence of this mark does not imply that async test functions inside the class are collected by pytest-asyncio.
38+
That means the presence of this mark does not imply that async test functions inside the class or module are collected by pytest-asyncio.
3939
The collection happens automatically in `auto` mode.
4040
However, if you're using strict mode, you still have to apply the `asyncio` mark to your async test functions.
4141

@@ -79,8 +79,33 @@ In *auto* mode, the ``pytest.mark.asyncio`` marker can be omitted:
7979
async def test_this_runs_in_same_loop(self):
8080
assert asyncio.get_running_loop() is TestClassScopedLoop.loop
8181
82+
Similarly, a module-scoped loop is provided when adding the _asyncio_event_loop_ mark to the module:
8283

84+
.. code-block:: python
85+
86+
import asyncio
87+
88+
import pytest
89+
90+
pytestmark = pytest.mark.asyncio_event_loop
91+
92+
loop: asyncio.AbstractEventLoop
8393
8494
95+
async def test_remember_loop():
96+
global loop
97+
loop = asyncio.get_running_loop()
98+
99+
100+
async def test_this_runs_in_same_loop():
101+
global loop
102+
assert asyncio.get_running_loop() is loop
103+
104+
105+
class TestClassA:
106+
async def test_this_runs_in_same_loop(self):
107+
global loop
108+
assert asyncio.get_running_loop() is loop
109+
85110
.. |pytestmark| replace:: ``pytestmark``
86111
.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules

pytest_asyncio/plugin.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ def pytest_configure(config: Config) -> None:
180180
config.addinivalue_line(
181181
"markers",
182182
"asyncio_event_loop: "
183-
"Provides an asyncio event loop in the scope of the marked test class",
183+
"Provides an asyncio event loop in the scope of the marked test "
184+
"class or module",
184185
)
185186

186187

@@ -347,7 +348,7 @@ def pytest_pycollect_makeitem(
347348

348349
@pytest.hookimpl
349350
def pytest_collectstart(collector: pytest.Collector):
350-
if not isinstance(collector, pytest.Class):
351+
if not isinstance(collector, (pytest.Class, pytest.Module)):
351352
return
352353
# pytest.Collector.own_markers is empty at this point,
353354
# so we rely on _pytest.mark.structures.get_unpacked_marks
@@ -357,17 +358,20 @@ def pytest_collectstart(collector: pytest.Collector):
357358
continue
358359

359360
@pytest.fixture(
360-
scope="class",
361+
scope="class" if isinstance(collector, pytest.Class) else "module",
361362
name="event_loop",
362363
)
363-
def scoped_event_loop(cls) -> Iterator[asyncio.AbstractEventLoop]:
364+
def scoped_event_loop(
365+
*args, # Function needs to accept "cls" when collected by pytest.Class
366+
) -> Iterator[asyncio.AbstractEventLoop]:
364367
loop = asyncio.get_event_loop_policy().new_event_loop()
365368
yield loop
366369
loop.close()
367370

368371
# @pytest.fixture does not register the fixture anywhere, so pytest doesn't
369372
# know it exists. We work around this by attaching the fixture function to the
370373
# collected Python class, where it will be picked up by pytest.Class.collect()
374+
# or pytest.Module.collect(), respectively
371375
collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop
372376
break
373377

tests/markers/test_module_marker.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,66 @@ def sample_fixture():
5050
)
5151
result = pytester.runpytest("--asyncio-mode=strict")
5252
result.assert_outcomes(passed=2)
53+
54+
55+
def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester):
56+
pytester.makepyfile(
57+
dedent(
58+
"""\
59+
import asyncio
60+
import pytest
61+
62+
pytestmark = pytest.mark.asyncio_event_loop
63+
64+
loop: asyncio.AbstractEventLoop
65+
66+
@pytest.mark.asyncio
67+
async def test_remember_loop():
68+
global loop
69+
loop = asyncio.get_running_loop()
70+
71+
@pytest.mark.asyncio
72+
async def test_this_runs_in_same_loop():
73+
global loop
74+
assert asyncio.get_running_loop() is loop
75+
76+
class TestClassA:
77+
@pytest.mark.asyncio
78+
async def test_this_runs_in_same_loop(self):
79+
global loop
80+
assert asyncio.get_running_loop() is loop
81+
"""
82+
)
83+
)
84+
result = pytester.runpytest("--asyncio-mode=strict")
85+
result.assert_outcomes(passed=3)
86+
87+
88+
def test_asyncio_mark_provides_class_scoped_loop_auto_mode(pytester: Pytester):
89+
pytester.makepyfile(
90+
dedent(
91+
"""\
92+
import asyncio
93+
import pytest
94+
95+
pytestmark = pytest.mark.asyncio_event_loop
96+
97+
loop: asyncio.AbstractEventLoop
98+
99+
async def test_remember_loop():
100+
global loop
101+
loop = asyncio.get_running_loop()
102+
103+
async def test_this_runs_in_same_loop():
104+
global loop
105+
assert asyncio.get_running_loop() is loop
106+
107+
class TestClassA:
108+
async def test_this_runs_in_same_loop(self):
109+
global loop
110+
assert asyncio.get_running_loop() is loop
111+
"""
112+
)
113+
)
114+
result = pytester.runpytest("--asyncio-mode=auto")
115+
result.assert_outcomes(passed=3)

0 commit comments

Comments
 (0)