Skip to content

Commit

Permalink
[feat] Introduce the event_loop_policy fixture.
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Seifert <[email protected]>
  • Loading branch information
seifertm committed Nov 8, 2023
1 parent e55317a commit 25343df
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 29 deletions.
11 changes: 8 additions & 3 deletions docs/source/how-to-guides/uvloop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
How to test with uvloop
=======================

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:
Replace the default event loop policy in your *conftest.py:*

.. code-block:: python
import asyncio
import pytest
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
@pytest.fixture(scope="session")
def event_loop_policy():
return uvloop.EventLoopPolicy()
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.
4 changes: 2 additions & 2 deletions docs/source/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
Changelog
=========

0.22.1 (UNRELEASED)
0.23.0 (UNRELEASED)
===================
- Fixes a bug that broke compatibility with pytest>=7.0,<7.2. `#654 <https://github.com/pytest-dev/pytest-asyncio/pull/654>`_
- 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>`_

0.22.0 (2023-10-31)
===================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import pytest


@pytest.mark.asyncio_event_loop(
policy=[
@pytest.fixture(
params=[
asyncio.DefaultEventLoopPolicy(),
asyncio.DefaultEventLoopPolicy(),
]
)
def event_loop_policy(request):
return request.param


class TestWithDifferentLoopPolicies:
@pytest.mark.asyncio
async def test_parametrized_loop(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
pass


@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
@pytest.fixture(scope="class")
def event_loop_policy(request):
return CustomEventLoopPolicy()


@pytest.mark.asyncio_event_loop
class TestUsesCustomEventLoopPolicy:
@pytest.mark.asyncio
async def test_uses_custom_event_loop_policy(self):
Expand Down
39 changes: 29 additions & 10 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import inspect
import socket
import warnings
from asyncio import AbstractEventLoopPolicy
from textwrap import dedent
from typing import (
Any,
Expand Down Expand Up @@ -553,12 +554,6 @@ def pytest_collectstart(collector: pytest.Collector):
for mark in marks:
if not mark.name == "asyncio_event_loop":
continue
event_loop_policy = mark.kwargs.get("policy", asyncio.get_event_loop_policy())
policy_params = (
event_loop_policy
if isinstance(event_loop_policy, Iterable)
else (event_loop_policy,)
)

# There seem to be issues when a fixture is shadowed by another fixture
# and both differ in their params.
Expand All @@ -573,14 +568,12 @@ def pytest_collectstart(collector: pytest.Collector):
@pytest.fixture(
scope="class" if isinstance(collector, pytest.Class) else "module",
name=event_loop_fixture_id,
params=policy_params,
ids=tuple(type(policy).__name__ for policy in policy_params),
)
def scoped_event_loop(
*args, # Function needs to accept "cls" when collected by pytest.Class
request,
event_loop_policy,
) -> Iterator[asyncio.AbstractEventLoop]:
new_loop_policy = request.param
new_loop_policy = event_loop_policy
old_loop_policy = asyncio.get_event_loop_policy()
old_loop = asyncio.get_event_loop()
asyncio.set_event_loop_policy(new_loop_policy)
Expand Down Expand Up @@ -675,6 +668,7 @@ def pytest_fixture_setup(
_add_finalizers(
fixturedef,
_close_event_loop,
_restore_event_loop_policy(asyncio.get_event_loop_policy()),
_provide_clean_event_loop,
)
outcome = yield
Expand Down Expand Up @@ -749,6 +743,23 @@ def _close_event_loop() -> None:
loop.close()


def _restore_event_loop_policy(previous_policy) -> Callable[[], None]:
def _restore_policy():
# Close any event loop associated with the old loop policy
# to avoid ResourceWarnings in the _provide_clean_event_loop finalizer
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
loop = previous_policy.get_event_loop()
except RuntimeError:
loop = None
if loop:
loop.close()
asyncio.set_event_loop_policy(previous_policy)

return _restore_policy


def _provide_clean_event_loop() -> None:
# At this point, the event loop for the current thread is closed.
# When a user calls asyncio.get_event_loop(), they will get a closed loop.
Expand Down Expand Up @@ -856,6 +867,8 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
@pytest.fixture
def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
"""Create an instance of the default event loop for each test case."""
new_loop_policy = request.getfixturevalue(event_loop_policy.__name__)
asyncio.set_event_loop_policy(new_loop_policy)
loop = asyncio.get_event_loop_policy().new_event_loop()
# Add a magic value to the event loop, so pytest-asyncio can determine if the
# event_loop fixture was overridden. Other implementations of event_loop don't
Expand All @@ -867,6 +880,12 @@ def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
loop.close()


@pytest.fixture(scope="session", autouse=True)
def event_loop_policy() -> AbstractEventLoopPolicy:
"""Return an instance of the policy used to create asyncio event loops."""
return asyncio.get_event_loop_policy()


def _unused_port(socket_type: int) -> int:
"""Find an unused localhost port from 1024-65535 and return it."""
with contextlib.closing(socket.socket(type=socket_type)) as sock:
Expand Down
17 changes: 12 additions & 5 deletions tests/markers/test_class_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,12 @@ def test_asyncio_event_loop_mark_allows_specifying_the_loop_policy(
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
pass
@pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
class TestUsesCustomEventLoopPolicy:
@pytest.fixture(scope="class")
def event_loop_policy():
return CustomEventLoopPolicy()
@pytest.mark.asyncio_event_loop
class TestUsesCustomEventLoop:
@pytest.mark.asyncio
async def test_uses_custom_event_loop_policy(self):
Expand Down Expand Up @@ -173,15 +177,18 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies(
import pytest
@pytest.mark.asyncio_event_loop(
policy=[
@pytest.fixture(
params=[
asyncio.DefaultEventLoopPolicy(),
asyncio.DefaultEventLoopPolicy(),
]
)
def event_loop_policy(request):
return request.param
class TestWithDifferentLoopPolicies:
@pytest.mark.asyncio
async def test_parametrized_loop(self):
async def test_parametrized_loop(self, request):
pass
"""
)
Expand Down
147 changes: 147 additions & 0 deletions tests/markers/test_function_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from textwrap import dedent

from pytest import Pytester


def test_asyncio_mark_provides_function_scoped_loop_strict_mode(pytester: Pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
pytestmark = pytest.mark.asyncio
loop: asyncio.AbstractEventLoop
async def test_remember_loop():
global loop
loop = asyncio.get_running_loop()
async def test_does_not_run_in_same_loop():
global loop
assert asyncio.get_running_loop() is not loop
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=2)


def test_function_scope_supports_explicit_event_loop_fixture_request(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import pytest
pytestmark = pytest.mark.asyncio
async def test_remember_loop(event_loop):
pass
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
'*is asynchronous and explicitly requests the "event_loop" fixture*'
)


def test_asyncio_mark_respects_the_loop_policy(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
pytestmark = pytest.mark.asyncio
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
pass
@pytest.fixture(scope="function")
def event_loop_policy():
return CustomEventLoopPolicy()
async def test_uses_custom_event_loop_policy():
assert isinstance(
asyncio.get_event_loop_policy(),
CustomEventLoopPolicy,
)
"""
),
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_asyncio_mark_respects_parametrized_loop_policies(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
pytestmark = pytest.mark.asyncio
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
pass
@pytest.fixture(
scope="module",
params=[
CustomEventLoopPolicy(),
CustomEventLoopPolicy(),
],
)
def event_loop_policy(request):
return request.param
async def test_parametrized_loop():
assert isinstance(
asyncio.get_event_loop_policy(),
CustomEventLoopPolicy,
)
"""
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result.assert_outcomes(passed=2)


def test_asyncio_mark_provides_function_scoped_loop_to_fixtures(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
import pytest_asyncio
pytestmark = pytest.mark.asyncio
loop: asyncio.AbstractEventLoop
@pytest_asyncio.fixture
async def my_fixture():
global loop
loop = asyncio.get_running_loop()
async def test_runs_is_same_loop_as_fixture(my_fixture):
global loop
assert asyncio.get_running_loop() is loop
"""
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
21 changes: 15 additions & 6 deletions tests/markers/test_module_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@ class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
from .custom_policy import CustomEventLoopPolicy
pytestmark = pytest.mark.asyncio_event_loop(policy=CustomEventLoopPolicy())
pytestmark = pytest.mark.asyncio_event_loop
@pytest.fixture(scope="module")
def event_loop_policy():
return CustomEventLoopPolicy()
@pytest.mark.asyncio
async def test_uses_custom_event_loop_policy():
Expand All @@ -178,7 +182,7 @@ async def test_uses_custom_event_loop_policy():
async def test_does_not_use_custom_event_loop_policy():
assert not isinstance(
asyncio.get_event_loop_policy(),
CustomEventLoopPolicy,
CustomEventLoopPolicy,
)
"""
),
Expand All @@ -197,20 +201,25 @@ def test_asyncio_event_loop_mark_allows_specifying_multiple_loop_policies(
import pytest
pytestmark = pytest.mark.asyncio_event_loop(
policy=[
pytestmark = pytest.mark.asyncio_event_loop
@pytest.fixture(
scope="module",
params=[
asyncio.DefaultEventLoopPolicy(),
asyncio.DefaultEventLoopPolicy(),
]
],
)
def event_loop_policy(request):
return request.param
@pytest.mark.asyncio
async def test_parametrized_loop():
pass
"""
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result = pytester.runpytest_subprocess("--asyncio-mode=strict", "--setup-show")
result.assert_outcomes(passed=2)


Expand Down

0 comments on commit 25343df

Please sign in to comment.