Skip to content

Commit 57d60d0

Browse files
committed
[feat] The asyncio_event_loop mark specifying an optional event loop policy.
Signed-off-by: Michael Seifert <[email protected]>
1 parent c8a52a0 commit 57d60d0

File tree

4 files changed

+146
-7
lines changed

4 files changed

+146
-7
lines changed

docs/source/reference/markers.rst

+21
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,26 @@ Similarly, a module-scoped loop is provided when adding the `asyncio_event_loop`
107107
global loop
108108
assert asyncio.get_running_loop() is loop
109109
110+
The `asyncio_event_loop` mark supports an optional `policy` keyword argument to set the asyncio event loop policy.
111+
112+
.. code-block:: python
113+
114+
import asyncio
115+
116+
import pytest
117+
118+
119+
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
120+
pass
121+
122+
123+
@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
124+
class TestUsesCustomEventLoopPolicy:
125+
@pytest.mark.asyncio
126+
async def test_uses_custom_event_loop_policy(self):
127+
assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy)
128+
129+
If no explicit policy is provided, the mark will use the loop policy returned by ``asyncio.get_event_loop_policy()``.
130+
110131
.. |pytestmark| replace:: ``pytestmark``
111132
.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules

pytest_asyncio/plugin.py

+40-7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
FixtureRequest,
3232
Function,
3333
Item,
34+
Metafunc,
3435
Parser,
3536
PytestPluginManager,
3637
Session,
@@ -367,6 +368,7 @@ def pytest_collectstart(collector: pytest.Collector):
367368
for mark in marks:
368369
if not mark.name == "asyncio_event_loop":
369370
continue
371+
event_loop_policy = mark.kwargs.get("policy", asyncio.get_event_loop_policy())
370372

371373
# There seem to be issues when a fixture is shadowed by another fixture
372374
# and both differ in their params.
@@ -381,13 +383,23 @@ def pytest_collectstart(collector: pytest.Collector):
381383
@pytest.fixture(
382384
scope="class" if isinstance(collector, pytest.Class) else "module",
383385
name=event_loop_fixture_id,
386+
params=(event_loop_policy,),
387+
ids=(type(event_loop_policy).__name__,),
384388
)
385389
def scoped_event_loop(
386390
*args, # Function needs to accept "cls" when collected by pytest.Class
391+
request,
387392
) -> Iterator[asyncio.AbstractEventLoop]:
388-
loop = asyncio.get_event_loop_policy().new_event_loop()
393+
new_loop_policy = request.param
394+
old_loop_policy = asyncio.get_event_loop_policy()
395+
old_loop = asyncio.get_event_loop()
396+
asyncio.set_event_loop_policy(new_loop_policy)
397+
loop = asyncio.new_event_loop()
398+
asyncio.set_event_loop(loop)
389399
yield loop
390400
loop.close()
401+
asyncio.set_event_loop_policy(old_loop_policy)
402+
asyncio.set_event_loop(old_loop)
391403

392404
# @pytest.fixture does not register the fixture anywhere, so pytest doesn't
393405
# know it exists. We work around this by attaching the fixture function to the
@@ -430,6 +442,33 @@ def _hypothesis_test_wraps_coroutine(function: Any) -> bool:
430442
return _is_coroutine(function.hypothesis.inner_test)
431443

432444

445+
@pytest.hookimpl(tryfirst=True)
446+
def pytest_generate_tests(metafunc: Metafunc) -> None:
447+
for event_loop_provider_node, _ in metafunc.definition.iter_markers_with_node(
448+
"asyncio_event_loop"
449+
):
450+
event_loop_fixture_id = event_loop_provider_node.stash.get(
451+
_event_loop_fixture_id, None
452+
)
453+
if event_loop_fixture_id:
454+
event_loop_fixture_id = event_loop_provider_node.stash[
455+
_event_loop_fixture_id
456+
]
457+
fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage")
458+
if "event_loop" in metafunc.fixturenames:
459+
raise MultipleEventLoopsRequestedError(
460+
_MULTIPLE_LOOPS_REQUESTED_ERROR
461+
% (metafunc.definition.nodeid, event_loop_provider_node.nodeid),
462+
)
463+
# Add the scoped event loop fixture to Metafunc's list of fixture names and
464+
# fixturedefs and leave the actual parametrization to pytest
465+
metafunc.fixturenames.insert(0, event_loop_fixture_id)
466+
metafunc._arg2fixturedefs[
467+
event_loop_fixture_id
468+
] = fixturemanager._arg2fixturedefs[event_loop_fixture_id]
469+
break
470+
471+
433472
@pytest.hookimpl(hookwrapper=True)
434473
def pytest_fixture_setup(
435474
fixturedef: FixtureDef, request: SubRequest
@@ -609,18 +648,12 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
609648
return
610649
event_loop_fixture_id = "event_loop"
611650
for node, mark in item.iter_markers_with_node("asyncio_event_loop"):
612-
scoped_event_loop_provider_node = node
613651
event_loop_fixture_id = node.stash.get(_event_loop_fixture_id, None)
614652
if event_loop_fixture_id:
615653
break
616654
fixturenames = item.fixturenames # type: ignore[attr-defined]
617655
# inject an event loop fixture for all async tests
618656
if "event_loop" in fixturenames:
619-
if event_loop_fixture_id != "event_loop":
620-
raise MultipleEventLoopsRequestedError(
621-
_MULTIPLE_LOOPS_REQUESTED_ERROR
622-
% (item.nodeid, scoped_event_loop_provider_node.nodeid),
623-
)
624657
fixturenames.remove("event_loop")
625658
fixturenames.insert(0, event_loop_fixture_id)
626659
obj = getattr(item, "obj", None)

tests/markers/test_class_marker.py

+35
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,38 @@ async def test_remember_loop(self, event_loop):
126126
result = pytester.runpytest("--asyncio-mode=strict")
127127
result.assert_outcomes(errors=1)
128128
result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *")
129+
130+
131+
def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy(
132+
pytester: pytest.Pytester,
133+
):
134+
pytester.makepyfile(
135+
dedent(
136+
"""\
137+
import asyncio
138+
import pytest
139+
140+
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
141+
pass
142+
143+
@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
144+
class TestUsesCustomEventLoopPolicy:
145+
146+
@pytest.mark.asyncio
147+
async def test_uses_custom_event_loop_policy(self):
148+
assert isinstance(
149+
asyncio.get_event_loop_policy(),
150+
CustomEventLoopPolicy,
151+
)
152+
153+
@pytest.mark.asyncio
154+
async def test_does_not_use_custom_event_loop_policy():
155+
assert not isinstance(
156+
asyncio.get_event_loop_policy(),
157+
CustomEventLoopPolicy,
158+
)
159+
"""
160+
)
161+
)
162+
result = pytester.runpytest("--asyncio-mode=strict")
163+
result.assert_outcomes(passed=2)

tests/markers/test_module_marker.py

+50
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,53 @@ async def test_remember_loop(event_loop):
135135
result = pytester.runpytest("--asyncio-mode=strict")
136136
result.assert_outcomes(errors=1)
137137
result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *")
138+
139+
140+
def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy(
141+
pytester: Pytester,
142+
):
143+
pytester.makepyfile(
144+
__init__="",
145+
custom_policy=dedent(
146+
"""\
147+
import asyncio
148+
149+
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
150+
pass
151+
"""
152+
),
153+
test_uses_custom_policy=dedent(
154+
"""\
155+
import asyncio
156+
import pytest
157+
158+
from .custom_policy import CustomEventLoopPolicy
159+
160+
pytestmark = pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
161+
162+
@pytest.mark.asyncio
163+
async def test_uses_custom_event_loop_policy():
164+
assert isinstance(
165+
asyncio.get_event_loop_policy(),
166+
CustomEventLoopPolicy,
167+
)
168+
"""
169+
),
170+
test_does_not_use_custom_policy=dedent(
171+
"""\
172+
import asyncio
173+
import pytest
174+
175+
from .custom_policy import CustomEventLoopPolicy
176+
177+
@pytest.mark.asyncio
178+
async def test_does_not_use_custom_event_loop_policy():
179+
assert not isinstance(
180+
asyncio.get_event_loop_policy(),
181+
CustomEventLoopPolicy,
182+
)
183+
"""
184+
),
185+
)
186+
result = pytester.runpytest("--asyncio-mode=strict")
187+
result.assert_outcomes(passed=2)

0 commit comments

Comments
 (0)