Skip to content

Commit 685560a

Browse files
committed
[feat] Deprecate event loop fixture overrides.
Signed-off-by: Michael Seifert <[email protected]>
1 parent 14d1c61 commit 685560a

File tree

7 files changed

+121
-23
lines changed

7 files changed

+121
-23
lines changed

docs/source/reference/changelog.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ Changelog
44

55
1.0.0 (UNRELEASED)
66
==================
7-
- Remove support for Python 3.7
8-
- Declare support for Python 3.12
97
- Class-scoped and module-scoped event loops can be requested
108
via the _asyncio_event_loop_ mark. `#620 <https://github.com/pytest-dev/pytest-asyncio/pull/620>`_
9+
- Deprecate redefinition of the `event_loop` fixture. `#587 <https://github.com/pytest-dev/pytest-asyncio/issues/531>`_
10+
Users requiring a class-scoped or module-scoped asyncio event loop for their tests
11+
should mark the corresponding class or module with `asyncio_event_loop`.
12+
- Remove support for Python 3.7
13+
- Declare support for Python 3.12
1114

1215
0.21.1 (2023-07-12)
1316
===================

docs/source/reference/fixtures.rst

+1-17
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,7 @@ to ``function`` scope.
1919
Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The `asyncio <#pytest-mark-asyncio>`__ marker
2020
is used to mark coroutines that should be treated as test functions.
2121

22-
The ``event_loop`` fixture can be overridden in any of the standard pytest locations,
23-
e.g. directly in the test file, or in ``conftest.py``. This allows redefining the
24-
fixture scope, for example:
25-
26-
.. code-block:: python
27-
28-
@pytest.fixture(scope="module")
29-
def event_loop():
30-
policy = asyncio.get_event_loop_policy()
31-
loop = policy.new_event_loop()
32-
yield loop
33-
loop.close()
34-
35-
When defining multiple ``event_loop`` fixtures, you should ensure that their scopes don't overlap.
36-
Each of the fixtures replace the running event loop, potentially without proper clean up.
37-
This will emit a warning and likely lead to errors in your tests suite.
38-
You can manually check for overlapping ``event_loop`` fixtures by running pytest with the ``--setup-show`` option.
22+
If your tests require an asyncio event loop with class or module scope, apply the `asyncio_event_loop mark <./markers.html/#pytest-mark-asyncio-event-loop>`__ to the respective class or module.
3923

4024
If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture.
4125

pytest_asyncio/plugin.py

+29
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,18 @@ def _hypothesis_test_wraps_coroutine(function: Any) -> bool:
461461
return _is_coroutine(function.hypothesis.inner_test)
462462

463463

464+
_REDEFINED_EVENT_LOOP_FIXTURE_WARNING = dedent(
465+
"""\
466+
The event_loop fixture provided by pytest-asyncio has been redefined in
467+
%s:%d
468+
Replacing the event_loop fixture with a custom implementation is deprecated
469+
and will lead to errors in the future.
470+
If you want to request an asyncio event loop with a class or module scope,
471+
please attach the asyncio_event_loop mark to the respective class or module.
472+
"""
473+
)
474+
475+
464476
@pytest.hookimpl(tryfirst=True)
465477
def pytest_generate_tests(metafunc: Metafunc) -> None:
466478
for event_loop_provider_node, _ in metafunc.definition.iter_markers_with_node(
@@ -497,6 +509,17 @@ def pytest_fixture_setup(
497509
) -> Optional[object]:
498510
"""Adjust the event loop policy when an event loop is produced."""
499511
if fixturedef.argname == "event_loop":
512+
# FixtureDef.baseid is an empty string when the Fixture was found in a plugin.
513+
# This is also true, when the fixture was defined in a conftest.py
514+
# at the rootdir.
515+
fixture_filename = inspect.getsourcefile(fixturedef.func)
516+
if not getattr(fixturedef.func, "__original_func", False):
517+
_, fixture_line_number = inspect.getsourcelines(fixturedef.func)
518+
warnings.warn(
519+
_REDEFINED_EVENT_LOOP_FIXTURE_WARNING
520+
% (fixture_filename, fixture_line_number),
521+
DeprecationWarning,
522+
)
500523
# The use of a fixture finalizer is preferred over the
501524
# pytest_fixture_post_finalizer hook. The fixture finalizer is invoked once
502525
# for each fixture, whereas the hook may be invoked multiple times for
@@ -691,6 +714,12 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
691714
@pytest.fixture
692715
def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
693716
"""Create an instance of the default event loop for each test case."""
717+
# Add a magic value to the fixture function, so that we can check for overrides
718+
# of this fixture in pytest_fixture_setup
719+
# The magic value must be part of the function definition, because pytest may have
720+
# multiple instances of the fixture function
721+
event_loop.__original_func = True
722+
694723
loop = asyncio.get_event_loop_policy().new_event_loop()
695724
yield loop
696725
loop.close()

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ asyncio_mode = auto
7171
junit_family=xunit2
7272
filterwarnings =
7373
error
74+
ignore:The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning
7475

7576
[flake8]
7677
max-line-length = 88

tests/test_event_loop_fixture_finalizer.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ async def test_ends_with_unclosed_loop():
111111
)
112112
)
113113
result = pytester.runpytest("--asyncio-mode=strict", "-W", "default")
114-
result.assert_outcomes(passed=1, warnings=1)
114+
result.assert_outcomes(passed=1, warnings=2)
115115
result.stdout.fnmatch_lines("*unclosed event loop*")
116116

117117

@@ -133,5 +133,5 @@ async def test_ends_with_unclosed_loop():
133133
)
134134
)
135135
result = pytester.runpytest("--asyncio-mode=strict", "-W", "default")
136-
result.assert_outcomes(passed=1, warnings=1)
136+
result.assert_outcomes(passed=1, warnings=2)
137137
result.stdout.fnmatch_lines("*unclosed event loop*")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from textwrap import dedent
2+
3+
from pytest import Pytester
4+
5+
6+
def test_emit_warning_when_event_loop_fixture_is_redefined(pytester: Pytester):
7+
pytester.makepyfile(
8+
dedent(
9+
"""\
10+
import asyncio
11+
import pytest
12+
13+
@pytest.fixture
14+
def event_loop():
15+
loop = asyncio.new_event_loop()
16+
yield loop
17+
loop.close()
18+
19+
@pytest.mark.asyncio
20+
async def test_emits_warning():
21+
pass
22+
23+
@pytest.mark.asyncio
24+
async def test_emits_warning_when_referenced_explicitly(event_loop):
25+
pass
26+
"""
27+
)
28+
)
29+
result = pytester.runpytest("--asyncio-mode=strict")
30+
result.assert_outcomes(passed=2, warnings=2)
31+
32+
33+
def test_does_not_emit_warning_when_no_test_uses_the_event_loop_fixture(
34+
pytester: Pytester,
35+
):
36+
pytester.makepyfile(
37+
dedent(
38+
"""\
39+
import asyncio
40+
import pytest
41+
42+
@pytest.fixture
43+
def event_loop():
44+
loop = asyncio.new_event_loop()
45+
yield loop
46+
loop.close()
47+
48+
def test_emits_no_warning():
49+
pass
50+
"""
51+
)
52+
)
53+
result = pytester.runpytest("--asyncio-mode=strict")
54+
result.assert_outcomes(passed=1, warnings=0)
55+
56+
57+
def test_emit_warning_when_redefined_event_loop_is_used_by_fixture(pytester: Pytester):
58+
pytester.makepyfile(
59+
dedent(
60+
"""\
61+
import asyncio
62+
import pytest
63+
import pytest_asyncio
64+
65+
@pytest.fixture
66+
def event_loop():
67+
loop = asyncio.new_event_loop()
68+
yield loop
69+
loop.close()
70+
71+
@pytest_asyncio.fixture
72+
async def uses_event_loop():
73+
pass
74+
75+
def test_emits_warning(uses_event_loop):
76+
pass
77+
"""
78+
)
79+
)
80+
result = pytester.runpytest("--asyncio-mode=strict")
81+
result.assert_outcomes(passed=1, warnings=1)

tests/test_multiloop.py

+2-2
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():
57+
async def test_for_custom_loop(event_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"
@@ -67,4 +67,4 @@ async def test_dependent_fixture(dependent_fixture):
6767
)
6868
)
6969
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
70-
result.assert_outcomes(passed=2)
70+
result.assert_outcomes(passed=2, warnings=2)

0 commit comments

Comments
 (0)