Skip to content

Commit 1f09cb5

Browse files
committed
[feat!] Provide a class-scoped asyncio event loop when a class has the asyncio mark.
Signed-off-by: Michael Seifert <[email protected]>
1 parent c99ef93 commit 1f09cb5

File tree

2 files changed

+78
-0
lines changed

2 files changed

+78
-0
lines changed

Diff for: pytest_asyncio/plugin.py

+28
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727

2828
import pytest
29+
from _pytest.mark.structures import get_unpacked_marks
2930
from pytest import (
3031
Config,
3132
FixtureRequest,
@@ -339,6 +340,33 @@ def pytest_pycollect_makeitem(
339340
return None
340341

341342

343+
@pytest.hookimpl
344+
def pytest_collectstart(collector: pytest.Collector):
345+
if not isinstance(collector, pytest.Class):
346+
return
347+
# pytest.Collector.own_markers is empty at this point,
348+
# so we rely on _pytest.mark.structures.get_unpacked_marks
349+
marks = get_unpacked_marks(collector.obj, consider_mro=True)
350+
for mark in marks:
351+
if not mark.name == "asyncio":
352+
continue
353+
354+
@pytest.fixture(
355+
scope="class",
356+
name="event_loop",
357+
)
358+
def scoped_event_loop(cls) -> Iterator[asyncio.AbstractEventLoop]:
359+
loop = asyncio.get_event_loop_policy().new_event_loop()
360+
yield loop
361+
loop.close()
362+
363+
# @pytest.fixture does not register the fixture anywhere, so pytest doesn't
364+
# know it exists. We work around this by attaching the fixture function to the
365+
# collected Python class, where it will be picked up by pytest.Class.collect()
366+
collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop
367+
break
368+
369+
342370
def pytest_collection_modifyitems(
343371
session: Session, config: Config, items: List[Item]
344372
) -> None:

Diff for: tests/markers/test_class_marker.py

+50
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Test if pytestmark works when defined on a class."""
22
import asyncio
3+
from textwrap import dedent
34

45
import pytest
56

@@ -23,3 +24,52 @@ async def inc():
2324
@pytest.fixture
2425
def sample_fixture():
2526
return None
27+
28+
29+
def test_asyncio_mark_provides_class_scoped_loop(pytester: pytest.Pytester):
30+
pytester.makepyfile(
31+
dedent(
32+
"""\
33+
import asyncio
34+
import pytest
35+
36+
@pytest.mark.asyncio
37+
class TestClassScopedLoop:
38+
loop: asyncio.AbstractEventLoop
39+
40+
async def test_remember_loop(self):
41+
TestClassScopedLoop.loop = asyncio.get_running_loop()
42+
43+
async def test_this_runs_in_same_loop(self):
44+
assert asyncio.get_running_loop() is TestClassScopedLoop.loop
45+
"""
46+
)
47+
)
48+
result = pytester.runpytest("--asyncio-mode=strict")
49+
result.assert_outcomes(passed=2)
50+
51+
52+
def test_asyncio_mark_is_inherited_to_subclasses(pytester: pytest.Pytester):
53+
pytester.makepyfile(
54+
dedent(
55+
"""\
56+
import asyncio
57+
import pytest
58+
59+
@pytest.mark.asyncio
60+
class TestSuperClassWithMark:
61+
pass
62+
63+
class TestWithoutMark(TestSuperClassWithMark):
64+
loop: asyncio.AbstractEventLoop
65+
66+
async def test_remember_loop(self):
67+
TestWithoutMark.loop = asyncio.get_running_loop()
68+
69+
async def test_this_runs_in_same_loop(self):
70+
assert asyncio.get_running_loop() is TestWithoutMark.loop
71+
"""
72+
)
73+
)
74+
result = pytester.runpytest("--asyncio-mode=strict")
75+
result.assert_outcomes(passed=2)

0 commit comments

Comments
 (0)