Skip to content

Commit 9d364a8

Browse files
committed
[feat] Introduce the event_loop_policy fixture.
Signed-off-by: Michael Seifert <[email protected]>
1 parent 973eaa4 commit 9d364a8

11 files changed

+281
-26
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

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55
0.23.0 (UNRELEASED)
66
===================
77
- Removes pytest-trio from the test dependencies `#620 <https://github.com/pytest-dev/pytest-asyncio/pull/620>`_
8+
- 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>`_
89

910
0.22.0 (2023-10-31)
1011
===================
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import asyncio
2+
3+
import pytest
4+
5+
6+
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
7+
pass
8+
9+
10+
@pytest.fixture(scope="module")
11+
def event_loop_policy(request):
12+
return CustomEventLoopPolicy()
13+
14+
15+
@pytest.mark.asyncio(scope="module")
16+
async def test_uses_custom_event_loop_policy():
17+
assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import asyncio
2+
from asyncio import DefaultEventLoopPolicy
3+
4+
import pytest
5+
6+
7+
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
8+
pass
9+
10+
11+
@pytest.fixture(
12+
params=(
13+
DefaultEventLoopPolicy(),
14+
CustomEventLoopPolicy(),
15+
),
16+
)
17+
def event_loop_policy(request):
18+
return request.param
19+
20+
21+
@pytest.mark.asyncio
22+
async def test_uses_custom_event_loop_policy():
23+
assert isinstance(asyncio.get_event_loop_policy(), DefaultEventLoopPolicy)

Diff for: docs/source/reference/fixtures/index.rst

+18
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ If you need to change the type of the event loop, prefer setting a custom event
2222
If the ``pytest.mark.asyncio`` decorator is applied to a test function, the ``event_loop``
2323
fixture will be requested automatically by the test function.
2424

25+
event_loop_policy
26+
=================
27+
Returns the event loop policy used to create asyncio event loops.
28+
The default return value is *asyncio.get_event_loop_policy().*
29+
30+
This fixture can be overridden when a different event loop policy should be used.
31+
32+
.. include:: event_loop_policy_example.py
33+
:code: python
34+
35+
Multiple policies can be provided via fixture parameters.
36+
The fixture is automatically applied to all pytest-asyncio tests.
37+
Therefore, all tests managed by pytest-asyncio are run once for each fixture parameter.
38+
The following example runs the test with different event loop policies.
39+
40+
.. include:: event_loop_policy_parametrized_example.py
41+
:code: python
42+
2543
unused_tcp_port
2644
===============
2745
Finds and yields a single unused TCP port on the localhost interface. Useful for

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.mark.asyncio_event_loop
144+
class TestUsesCustomEventLoop:
145+
146+
@pytest.fixture(scope="class")
147+
def event_loop_policy(self):
148+
return CustomEventLoopPolicy()
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
)

0 commit comments

Comments
 (0)