Skip to content

Commit 0352780

Browse files
committed
[feat] Introduce the event_loop_policy fixture.
Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de>
1 parent e55317a commit 0352780

8 files changed

+224
-28
lines changed

Diff for: docs/source/how-to-guides/uvloop.rst

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
How to test with uvloop
33
=======================
44

5+
Redefinig the *event_loop_policy* fixture will parametrize all async tests. The following example causes all async tests to run multiple times, once for each event loop in the fixture parameters:
56
Replace the default event loop policy in your *conftest.py:*
67

78
.. code-block:: python
89
9-
import asyncio
10-
10+
import pytest
1111
import uvloop
1212
13-
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
13+
14+
@pytest.fixture(scope="session")
15+
def event_loop_policy():
16+
return uvloop.EventLoopPolicy()
17+
18+
You may choose to limit the scope of the fixture to *package,* *module,* or *class,* if you only want a subset of your tests to run with uvloop.

Diff for: docs/source/reference/changelog.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
Changelog
33
=========
44

5-
0.22.1 (UNRELEASED)
5+
0.23.0 (UNRELEASED)
66
===================
7-
- Fixes a bug that broke compatibility with pytest>=7.0,<7.2. `#654 <https://github.com/pytest-dev/pytest-asyncio/pull/654>`_
7+
- Introduces the *event_loop_policy* fixture which allows testing with non-default or multiple event loops `#662 <https://github.com/pytest-dev/pytest-asyncio/pull/662>`_
88

99
0.22.0 (2023-10-31)
1010
===================

Diff for: docs/source/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import pytest
44

55

6-
@pytest.mark.asyncio_event_loop(
7-
policy=[
6+
@pytest.fixture(
7+
params=[
88
asyncio.DefaultEventLoopPolicy(),
99
asyncio.DefaultEventLoopPolicy(),
1010
]
1111
)
12+
def event_loop_policy(request):
13+
return request.param
14+
15+
1216
class TestWithDifferentLoopPolicies:
1317
@pytest.mark.asyncio
1418
async def test_parametrized_loop(self):

Diff for: docs/source/reference/markers/class_scoped_loop_custom_policy_strict_mode_example.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
77
pass
88

99

10-
@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
10+
@pytest.fixture(scope="class")
11+
def event_loop_policy(request):
12+
return CustomEventLoopPolicy()
13+
14+
15+
@pytest.mark.asyncio_event_loop
1116
class TestUsesCustomEventLoopPolicy:
1217
@pytest.mark.asyncio
1318
async def test_uses_custom_event_loop_policy(self):

Diff for: pytest_asyncio/plugin.py

+29-10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import inspect
77
import socket
88
import warnings
9+
from asyncio import AbstractEventLoopPolicy
910
from textwrap import dedent
1011
from typing import (
1112
Any,
@@ -553,12 +554,6 @@ def pytest_collectstart(collector: pytest.Collector):
553554
for mark in marks:
554555
if not mark.name == "asyncio_event_loop":
555556
continue
556-
event_loop_policy = mark.kwargs.get("policy", asyncio.get_event_loop_policy())
557-
policy_params = (
558-
event_loop_policy
559-
if isinstance(event_loop_policy, Iterable)
560-
else (event_loop_policy,)
561-
)
562557

563558
# There seem to be issues when a fixture is shadowed by another fixture
564559
# and both differ in their params.
@@ -573,14 +568,12 @@ def pytest_collectstart(collector: pytest.Collector):
573568
@pytest.fixture(
574569
scope="class" if isinstance(collector, pytest.Class) else "module",
575570
name=event_loop_fixture_id,
576-
params=policy_params,
577-
ids=tuple(type(policy).__name__ for policy in policy_params),
578571
)
579572
def scoped_event_loop(
580573
*args, # Function needs to accept "cls" when collected by pytest.Class
581-
request,
574+
event_loop_policy,
582575
) -> Iterator[asyncio.AbstractEventLoop]:
583-
new_loop_policy = request.param
576+
new_loop_policy = event_loop_policy
584577
old_loop_policy = asyncio.get_event_loop_policy()
585578
old_loop = asyncio.get_event_loop()
586579
asyncio.set_event_loop_policy(new_loop_policy)
@@ -675,6 +668,7 @@ def pytest_fixture_setup(
675668
_add_finalizers(
676669
fixturedef,
677670
_close_event_loop,
671+
_restore_event_loop_policy(asyncio.get_event_loop_policy()),
678672
_provide_clean_event_loop,
679673
)
680674
outcome = yield
@@ -749,6 +743,23 @@ def _close_event_loop() -> None:
749743
loop.close()
750744

751745

746+
def _restore_event_loop_policy(previous_policy) -> Callable[[], None]:
747+
def _restore_policy():
748+
# Close any event loop associated with the old loop policy
749+
# to avoid ResourceWarnings in the _provide_clean_event_loop finalizer
750+
try:
751+
with warnings.catch_warnings():
752+
warnings.simplefilter("ignore", DeprecationWarning)
753+
loop = previous_policy.get_event_loop()
754+
except RuntimeError:
755+
loop = None
756+
if loop:
757+
loop.close()
758+
asyncio.set_event_loop_policy(previous_policy)
759+
760+
return _restore_policy
761+
762+
752763
def _provide_clean_event_loop() -> None:
753764
# At this point, the event loop for the current thread is closed.
754765
# When a user calls asyncio.get_event_loop(), they will get a closed loop.
@@ -856,6 +867,8 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
856867
@pytest.fixture
857868
def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
858869
"""Create an instance of the default event loop for each test case."""
870+
new_loop_policy = request.getfixturevalue(event_loop_policy.__name__)
871+
asyncio.set_event_loop_policy(new_loop_policy)
859872
loop = asyncio.get_event_loop_policy().new_event_loop()
860873
# Add a magic value to the event loop, so pytest-asyncio can determine if the
861874
# event_loop fixture was overridden. Other implementations of event_loop don't
@@ -867,6 +880,12 @@ def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
867880
loop.close()
868881

869882

883+
@pytest.fixture(scope="session", autouse=True)
884+
def event_loop_policy() -> AbstractEventLoopPolicy:
885+
"""Return an instance of the policy used to create asyncio event loops."""
886+
return asyncio.get_event_loop_policy()
887+
888+
870889
def _unused_port(socket_type: int) -> int:
871890
"""Find an unused localhost port from 1024-65535 and return it."""
872891
with contextlib.closing(socket.socket(type=socket_type)) as sock:

Diff for: tests/markers/test_class_marker.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,12 @@ def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy(
140140
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
141141
pass
142142
143-
@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
144-
class TestUsesCustomEventLoopPolicy:
143+
@pytest.fixture(scope="class")
144+
def event_loop_policy():
145+
return CustomEventLoopPolicy()
146+
147+
@pytest.mark.asyncio_event_loop
148+
class TestUsesCustomEventLoop:
145149
146150
@pytest.mark.asyncio
147151
async def test_uses_custom_event_loop_policy(self):
@@ -173,15 +177,18 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies(
173177
174178
import pytest
175179
176-
@pytest.mark.asyncio_event_loop(
177-
policy=[
180+
@pytest.fixture(
181+
params=[
178182
asyncio.DefaultEventLoopPolicy(),
179183
asyncio.DefaultEventLoopPolicy(),
180184
]
181185
)
186+
def event_loop_policy(request):
187+
return request.param
188+
182189
class TestWithDifferentLoopPolicies:
183190
@pytest.mark.asyncio
184-
async def test_parametrized_loop(self):
191+
async def test_parametrized_loop(self, request):
185192
pass
186193
"""
187194
)

Diff for: tests/markers/test_function_scope.py

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from textwrap import dedent
2+
3+
from pytest import Pytester
4+
5+
6+
def test_asyncio_mark_provides_function_scoped_loop_strict_mode(pytester: Pytester):
7+
pytester.makepyfile(
8+
dedent(
9+
"""\
10+
import asyncio
11+
import pytest
12+
13+
pytestmark = pytest.mark.asyncio
14+
15+
loop: asyncio.AbstractEventLoop
16+
17+
async def test_remember_loop():
18+
global loop
19+
loop = asyncio.get_running_loop()
20+
21+
async def test_does_not_run_in_same_loop():
22+
global loop
23+
assert asyncio.get_running_loop() is not loop
24+
"""
25+
)
26+
)
27+
result = pytester.runpytest("--asyncio-mode=strict")
28+
result.assert_outcomes(passed=2)
29+
30+
31+
def test_function_scope_supports_explicit_event_loop_fixture_request(
32+
pytester: Pytester,
33+
):
34+
pytester.makepyfile(
35+
dedent(
36+
"""\
37+
import pytest
38+
39+
pytestmark = pytest.mark.asyncio
40+
41+
async def test_remember_loop(event_loop):
42+
pass
43+
"""
44+
)
45+
)
46+
result = pytester.runpytest("--asyncio-mode=strict")
47+
result.assert_outcomes(passed=1, warnings=1)
48+
result.stdout.fnmatch_lines(
49+
'*is asynchronous and explicitly requests the "event_loop" fixture*'
50+
)
51+
52+
53+
def test_asyncio_mark_respects_the_loop_policy(
54+
pytester: Pytester,
55+
):
56+
pytester.makepyfile(
57+
dedent(
58+
"""\
59+
import asyncio
60+
import pytest
61+
62+
pytestmark = pytest.mark.asyncio
63+
64+
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
65+
pass
66+
67+
@pytest.fixture(scope="function")
68+
def event_loop_policy():
69+
return CustomEventLoopPolicy()
70+
71+
async def test_uses_custom_event_loop_policy():
72+
assert isinstance(
73+
asyncio.get_event_loop_policy(),
74+
CustomEventLoopPolicy,
75+
)
76+
"""
77+
),
78+
)
79+
result = pytester.runpytest("--asyncio-mode=strict")
80+
result.assert_outcomes(passed=1)
81+
82+
83+
def test_asyncio_mark_respects_parametrized_loop_policies(
84+
pytester: Pytester,
85+
):
86+
pytester.makepyfile(
87+
dedent(
88+
"""\
89+
import asyncio
90+
91+
import pytest
92+
93+
pytestmark = pytest.mark.asyncio
94+
95+
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
96+
pass
97+
98+
@pytest.fixture(
99+
scope="module",
100+
params=[
101+
CustomEventLoopPolicy(),
102+
CustomEventLoopPolicy(),
103+
],
104+
)
105+
def event_loop_policy(request):
106+
return request.param
107+
108+
async def test_parametrized_loop():
109+
assert isinstance(
110+
asyncio.get_event_loop_policy(),
111+
CustomEventLoopPolicy,
112+
)
113+
"""
114+
)
115+
)
116+
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
117+
result.assert_outcomes(passed=2)
118+
119+
120+
def test_asyncio_mark_provides_function_scoped_loop_to_fixtures(
121+
pytester: Pytester,
122+
):
123+
pytester.makepyfile(
124+
dedent(
125+
"""\
126+
import asyncio
127+
128+
import pytest
129+
import pytest_asyncio
130+
131+
pytestmark = pytest.mark.asyncio
132+
133+
loop: asyncio.AbstractEventLoop
134+
135+
@pytest_asyncio.fixture
136+
async def my_fixture():
137+
global loop
138+
loop = asyncio.get_running_loop()
139+
140+
async def test_runs_is_same_loop_as_fixture(my_fixture):
141+
global loop
142+
assert asyncio.get_running_loop() is loop
143+
"""
144+
)
145+
)
146+
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
147+
result.assert_outcomes(passed=1)

Diff for: tests/markers/test_module_marker.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
157157
158158
from .custom_policy import CustomEventLoopPolicy
159159
160-
pytestmark = pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
160+
pytestmark = pytest.mark.asyncio_event_loop
161+
162+
@pytest.fixture(scope="module")
163+
def event_loop_policy():
164+
return CustomEventLoopPolicy()
161165
162166
@pytest.mark.asyncio
163167
async def test_uses_custom_event_loop_policy():
@@ -178,7 +182,7 @@ async def test_uses_custom_event_loop_policy():
178182
async def test_does_not_use_custom_event_loop_policy():
179183
assert not isinstance(
180184
asyncio.get_event_loop_policy(),
181-
CustomEventLoopPolicy,
185+
CustomEventLoopPolicy,
182186
)
183187
"""
184188
),
@@ -197,12 +201,17 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies(
197201
198202
import pytest
199203
200-
pytestmark = pytest.mark.asyncio_event_loop(
201-
policy=[
204+
pytestmark = pytest.mark.asyncio_event_loop
205+
206+
@pytest.fixture(
207+
scope="module",
208+
params=[
202209
asyncio.DefaultEventLoopPolicy(),
203210
asyncio.DefaultEventLoopPolicy(),
204-
]
211+
],
205212
)
213+
def event_loop_policy(request):
214+
return request.param
206215
207216
@pytest.mark.asyncio
208217
async def test_parametrized_loop():

0 commit comments

Comments
 (0)