Skip to content

Commit f3acd72

Browse files
committed
[feat] Emit a deprecation warning when the event_loop fixture is explicitly requested by a coroutine or an async fixture.
Signed-off-by: Michael Seifert <[email protected]>
1 parent c6c7763 commit f3acd72

11 files changed

+238
-28
lines changed

docs/source/concepts.rst

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ Concepts
44

55
asyncio event loops
66
===================
7-
pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via the ``event_loop`` fixture, which is automatically requested by all async tests.
7+
pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via ``asyncio.get_running_loop()``.
88

99
.. code-block:: python
1010
11-
async def test_provided_loop_is_running_loop(event_loop):
12-
assert event_loop is asyncio.get_running_loop()
11+
async def test_runs_in_a_loop():
12+
assert asyncio.get_running_loop()
1313
14-
You can think of `event_loop` as an autouse fixture for async tests.
14+
Synchronous test functions can get access to an asyncio event loop via the `event_loop` fixture.
15+
16+
.. code-block:: python
17+
18+
def test_can_access_current_loop(event_loop):
19+
assert event_loop
1520
1621
Test discovery modes
1722
====================

docs/source/reference/fixtures/index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Fixtures
55
event_loop
66
==========
77
Creates a new asyncio event loop based on the current event loop policy. The new loop
8-
is available as the return value of this fixture or via `asyncio.get_running_loop <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop>`__.
8+
is available as the return value of this fixture for synchronous functions, or via `asyncio.get_running_loop <https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop>`__ for asynchronous functions.
99
The event loop is closed when the fixture scope ends. The fixture scope defaults
1010
to ``function`` scope.
1111

pytest_asyncio/plugin.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
Metafunc,
3636
Parser,
3737
PytestCollectionWarning,
38+
PytestDeprecationWarning,
3839
PytestPluginManager,
3940
Session,
4041
StashKey,
@@ -222,6 +223,16 @@ def _preprocess_async_fixtures(
222223
# This applies to pytest_trio fixtures, for example
223224
continue
224225
_make_asyncio_fixture_function(func)
226+
function_signature = inspect.signature(func)
227+
if "event_loop" in function_signature.parameters:
228+
warnings.warn(
229+
PytestDeprecationWarning(
230+
f"{func.__name__} is asynchronous and explicitly "
231+
f'requests the "event_loop" fixture. Asynchronous fixtures and '
232+
f'test functions should use "asyncio.get_running_loop()" '
233+
f"instead."
234+
)
235+
)
225236
_inject_fixture_argnames(fixturedef, event_loop_fixture_id)
226237
_synchronize_async_fixture(fixturedef, event_loop_fixture_id)
227238
assert _is_asyncio_fixture_function(fixturedef.func)
@@ -372,7 +383,7 @@ def _from_function(cls, function: Function, /) -> Function:
372383
Instantiates this specific PytestAsyncioFunction type from the specified
373384
Function item.
374385
"""
375-
return cls.from_parent(
386+
subclass_instance = cls.from_parent(
376387
function.parent,
377388
name=function.name,
378389
callspec=getattr(function, "callspec", None),
@@ -381,6 +392,16 @@ def _from_function(cls, function: Function, /) -> Function:
381392
keywords=function.keywords,
382393
originalname=function.originalname,
383394
)
395+
subclassed_function_signature = inspect.signature(subclass_instance.obj)
396+
if "event_loop" in subclassed_function_signature.parameters:
397+
subclass_instance.warn(
398+
PytestDeprecationWarning(
399+
f"{subclass_instance.name} is asynchronous and explicitly "
400+
f'requests the "event_loop" fixture. Asynchronous fixtures and '
401+
f'test functions should use "asyncio.get_running_loop()" instead.'
402+
)
403+
)
404+
return subclass_instance
384405

385406
@staticmethod
386407
def _can_substitute(item: Function) -> bool:

tests/async_fixtures/test_async_fixtures_with_finalizer.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,22 @@ def event_loop():
2626

2727

2828
@pytest.fixture(scope="module")
29-
async def port_with_event_loop_finalizer(request, event_loop):
29+
async def port_with_event_loop_finalizer(request):
3030
def port_finalizer(finalizer):
3131
async def port_afinalizer():
3232
# await task using loop provided by event_loop fixture
3333
# RuntimeError is raised if task is created on a different loop
3434
await finalizer
3535

36-
event_loop.run_until_complete(port_afinalizer())
36+
asyncio.get_event_loop().run_until_complete(port_afinalizer())
3737

3838
worker = asyncio.ensure_future(asyncio.sleep(0.2))
3939
request.addfinalizer(functools.partial(port_finalizer, worker))
4040
return True
4141

4242

4343
@pytest.fixture(scope="module")
44-
async def port_with_get_event_loop_finalizer(request, event_loop):
44+
async def port_with_get_event_loop_finalizer(request):
4545
def port_finalizer(finalizer):
4646
async def port_afinalizer():
4747
# await task using current loop retrieved from the event loop policy

tests/async_fixtures/test_nested.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ async def async_inner_fixture():
1212

1313

1414
@pytest.fixture()
15-
async def async_fixture_outer(async_inner_fixture, event_loop):
15+
async def async_fixture_outer(async_inner_fixture):
1616
await asyncio.sleep(0.01)
1717
print("outer start")
1818
assert async_inner_fixture is True

tests/markers/test_class_marker.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class TestPyTestMark:
99
pytestmark = pytest.mark.asyncio
1010

11-
async def test_is_asyncio(self, event_loop, sample_fixture):
11+
async def test_is_asyncio(self, sample_fixture):
1212
assert asyncio.get_event_loop()
1313
counter = 1
1414

tests/test_event_loop_fixture_override_deprecation.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,45 @@ def event_loop():
1919
@pytest.mark.asyncio
2020
async def test_emits_warning():
2121
pass
22+
"""
23+
)
24+
)
25+
result = pytester.runpytest("--asyncio-mode=strict")
26+
result.assert_outcomes(passed=1, warnings=1)
27+
result.stdout.fnmatch_lines(
28+
["*event_loop fixture provided by pytest-asyncio has been redefined*"]
29+
)
30+
31+
32+
def test_emit_warning_when_event_loop_fixture_is_redefined_explicit_request(
33+
pytester: Pytester,
34+
):
35+
pytester.makepyfile(
36+
dedent(
37+
"""\
38+
import asyncio
39+
import pytest
40+
41+
@pytest.fixture
42+
def event_loop():
43+
loop = asyncio.new_event_loop()
44+
yield loop
45+
loop.close()
2246
2347
@pytest.mark.asyncio
24-
async def test_emits_warning_when_referenced_explicitly(event_loop):
48+
async def test_emits_warning_when_requested_explicitly(event_loop):
2549
pass
2650
"""
2751
)
2852
)
2953
result = pytester.runpytest("--asyncio-mode=strict")
30-
result.assert_outcomes(passed=2, warnings=2)
54+
result.assert_outcomes(passed=1, warnings=2)
55+
result.stdout.fnmatch_lines(
56+
["*event_loop fixture provided by pytest-asyncio has been redefined*"]
57+
)
58+
result.stdout.fnmatch_lines(
59+
['*is asynchronous and explicitly requests the "event_loop" fixture*']
60+
)
3161

3262

3363
def test_does_not_emit_warning_when_no_test_uses_the_event_loop_fixture(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from textwrap import dedent
2+
3+
from pytest import Pytester
4+
5+
6+
def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine(
7+
pytester: Pytester,
8+
):
9+
pytester.makepyfile(
10+
dedent(
11+
"""\
12+
import pytest
13+
14+
@pytest.mark.asyncio
15+
async def test_coroutine_emits_warning(event_loop):
16+
pass
17+
"""
18+
)
19+
)
20+
result = pytester.runpytest("--asyncio-mode=strict")
21+
result.assert_outcomes(passed=1, warnings=1)
22+
result.stdout.fnmatch_lines(
23+
['*is asynchronous and explicitly requests the "event_loop" fixture*']
24+
)
25+
26+
27+
def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_method(
28+
pytester: Pytester,
29+
):
30+
pytester.makepyfile(
31+
dedent(
32+
"""\
33+
import pytest
34+
35+
class TestEmitsWarning:
36+
@pytest.mark.asyncio
37+
async def test_coroutine_emits_warning(self, event_loop):
38+
pass
39+
"""
40+
)
41+
)
42+
result = pytester.runpytest("--asyncio-mode=strict")
43+
result.assert_outcomes(passed=1, warnings=1)
44+
result.stdout.fnmatch_lines(
45+
['*is asynchronous and explicitly requests the "event_loop" fixture*']
46+
)
47+
48+
49+
def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_staticmethod(
50+
pytester: Pytester,
51+
):
52+
pytester.makepyfile(
53+
dedent(
54+
"""\
55+
import pytest
56+
57+
class TestEmitsWarning:
58+
@staticmethod
59+
@pytest.mark.asyncio
60+
async def test_coroutine_emits_warning(event_loop):
61+
pass
62+
"""
63+
)
64+
)
65+
result = pytester.runpytest("--asyncio-mode=strict")
66+
result.assert_outcomes(passed=1, warnings=1)
67+
result.stdout.fnmatch_lines(
68+
['*is asynchronous and explicitly requests the "event_loop" fixture*']
69+
)
70+
71+
72+
def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_fixture(
73+
pytester: Pytester,
74+
):
75+
pytester.makepyfile(
76+
dedent(
77+
"""\
78+
import pytest
79+
import pytest_asyncio
80+
81+
@pytest_asyncio.fixture
82+
async def emits_warning(event_loop):
83+
pass
84+
85+
@pytest.mark.asyncio
86+
async def test_uses_fixture(emits_warning):
87+
pass
88+
"""
89+
)
90+
)
91+
result = pytester.runpytest("--asyncio-mode=strict")
92+
result.assert_outcomes(passed=1, warnings=1)
93+
result.stdout.fnmatch_lines(
94+
['*is asynchronous and explicitly requests the "event_loop" fixture*']
95+
)
96+
97+
98+
def test_emit_warning_when_event_loop_is_explicitly_requested_in_async_gen_fixture(
99+
pytester: Pytester,
100+
):
101+
pytester.makepyfile(
102+
dedent(
103+
"""\
104+
import pytest
105+
import pytest_asyncio
106+
107+
@pytest_asyncio.fixture
108+
async def emits_warning(event_loop):
109+
yield
110+
111+
@pytest.mark.asyncio
112+
async def test_uses_fixture(emits_warning):
113+
pass
114+
"""
115+
)
116+
)
117+
result = pytester.runpytest("--asyncio-mode=strict")
118+
result.assert_outcomes(passed=1, warnings=1)
119+
result.stdout.fnmatch_lines(
120+
['*is asynchronous and explicitly requests the "event_loop" fixture*']
121+
)
122+
123+
124+
def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_function(
125+
pytester: Pytester,
126+
):
127+
pytester.makepyfile(
128+
dedent(
129+
"""\
130+
import pytest
131+
132+
def test_uses_fixture(event_loop):
133+
pass
134+
"""
135+
)
136+
)
137+
result = pytester.runpytest("--asyncio-mode=strict")
138+
result.assert_outcomes(passed=1)
139+
140+
141+
def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_fixture(
142+
pytester: Pytester,
143+
):
144+
pytester.makepyfile(
145+
dedent(
146+
"""\
147+
import pytest
148+
149+
@pytest.fixture
150+
def any_fixture(event_loop):
151+
pass
152+
153+
def test_uses_fixture(any_fixture):
154+
pass
155+
"""
156+
)
157+
)
158+
result = pytester.runpytest("--asyncio-mode=strict")
159+
result.assert_outcomes(passed=1)

tests/test_multiloop.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def event_loop():
5454
5555
5656
@pytest.mark.asyncio
57-
async def test_for_custom_loop(event_loop):
57+
async def test_for_custom_loop():
5858
"""This test should be executed using the custom loop."""
5959
await asyncio.sleep(0.01)
6060
assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"

0 commit comments

Comments
 (0)