Skip to content

Commit

Permalink
[feat] Emit a deprecation warning when the event_loop fixture is expl…
Browse files Browse the repository at this point in the history
…icitly requested by a coroutine or an async fixture.

Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de>
  • Loading branch information
seifertm committed Oct 31, 2023
1 parent 9f75b3c commit 044e568
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 28 deletions.
13 changes: 9 additions & 4 deletions docs/source/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ Concepts

asyncio event loops
===================
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.
pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via ``asyncio.get_running_loop()``.

.. code-block:: python
async def test_provided_loop_is_running_loop(event_loop):
assert event_loop is asyncio.get_running_loop()
async def test_runs_in_a_loop():
assert asyncio.get_running_loop()
You can think of `event_loop` as an autouse fixture for async tests.
Synchronous test functions can get access to an asyncio event loop via the `event_loop` fixture.

.. code-block:: python
def test_can_access_current_loop(event_loop):
assert event_loop
Test discovery modes
====================
Expand Down
2 changes: 1 addition & 1 deletion docs/source/reference/fixtures/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Fixtures
event_loop
==========
Creates a new asyncio event loop based on the current event loop policy. The new loop
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>`__.
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.
The event loop is closed when the fixture scope ends. The fixture scope defaults
to ``function`` scope.

Expand Down
23 changes: 22 additions & 1 deletion pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
Metafunc,
Parser,
PytestCollectionWarning,
PytestDeprecationWarning,
PytestPluginManager,
Session,
StashKey,
Expand Down Expand Up @@ -222,6 +223,16 @@ def _preprocess_async_fixtures(
# This applies to pytest_trio fixtures, for example
continue
_make_asyncio_fixture_function(func)
function_signature = inspect.signature(func)
if "event_loop" in function_signature.parameters:
warnings.warn(
PytestDeprecationWarning(
f"{func.__name__} is asynchronous and explicitly "
f'requests the "event_loop" fixture. Asynchronous fixtures and '
f'test functions should use "asyncio.get_running_loop()" '
f"instead."
)
)
_inject_fixture_argnames(fixturedef, event_loop_fixture_id)
_synchronize_async_fixture(fixturedef, event_loop_fixture_id)
assert _is_asyncio_fixture_function(fixturedef.func)
Expand Down Expand Up @@ -372,7 +383,7 @@ def _from_function(cls, function: Function, /) -> Function:
Instantiates this specific PytestAsyncioFunction type from the specified
Function item.
"""
return cls.from_parent(
subclass_instance = cls.from_parent(
function.parent,
name=function.name,
callspec=getattr(function, "callspec", None),
Expand All @@ -381,6 +392,16 @@ def _from_function(cls, function: Function, /) -> Function:
keywords=function.keywords,
originalname=function.originalname,
)
subclassed_function_signature = inspect.signature(subclass_instance.obj)
if "event_loop" in subclassed_function_signature.parameters:
subclass_instance.warn(
PytestDeprecationWarning(
f"{subclass_instance.name} is asynchronous and explicitly "
f'requests the "event_loop" fixture. Asynchronous fixtures and '
f'test functions should use "asyncio.get_running_loop()" instead.'
)
)
return subclass_instance

@staticmethod
def _can_substitute(item: Function) -> bool:
Expand Down
6 changes: 3 additions & 3 deletions tests/async_fixtures/test_async_fixtures_with_finalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,22 @@ def event_loop():


@pytest.fixture(scope="module")
async def port_with_event_loop_finalizer(request, event_loop):
async def port_with_event_loop_finalizer(request):
def port_finalizer(finalizer):
async def port_afinalizer():
# await task using loop provided by event_loop fixture
# RuntimeError is raised if task is created on a different loop
await finalizer

event_loop.run_until_complete(port_afinalizer())
asyncio.get_event_loop().run_until_complete(port_afinalizer())

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


@pytest.fixture(scope="module")
async def port_with_get_event_loop_finalizer(request, event_loop):
async def port_with_get_event_loop_finalizer(request):
def port_finalizer(finalizer):
async def port_afinalizer():
# await task using current loop retrieved from the event loop policy
Expand Down
2 changes: 1 addition & 1 deletion tests/async_fixtures/test_nested.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async def async_inner_fixture():


@pytest.fixture()
async def async_fixture_outer(async_inner_fixture, event_loop):
async def async_fixture_outer(async_inner_fixture):
await asyncio.sleep(0.01)
print("outer start")
assert async_inner_fixture is True
Expand Down
2 changes: 1 addition & 1 deletion tests/markers/test_class_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class TestPyTestMark:
pytestmark = pytest.mark.asyncio

async def test_is_asyncio(self, event_loop, sample_fixture):
async def test_is_asyncio(self, sample_fixture):
assert asyncio.get_event_loop()
counter = 1

Expand Down
34 changes: 32 additions & 2 deletions tests/test_event_loop_fixture_override_deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,45 @@ def event_loop():
@pytest.mark.asyncio
async def test_emits_warning():
pass
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1, warnings=1)
result.stdout.fnmatch_lines(
["*event_loop fixture provided by pytest-asyncio has been redefined*"]
)


def test_emit_warning_when_event_loop_fixture_is_redefined_explicit_request(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
@pytest.fixture
def event_loop():
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest.mark.asyncio
async def test_emits_warning_when_referenced_explicitly(event_loop):
async def test_emits_warning_when_requested_explicitly(event_loop):
pass
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=2, warnings=2)
result.assert_outcomes(passed=1, warnings=2)
result.stdout.fnmatch_lines(
["*event_loop fixture provided by pytest-asyncio has been redefined*"]
)
result.stdout.fnmatch_lines(
['*is asynchronous and explicitly requests the "event_loop" fixture*']
)


def test_does_not_emit_warning_when_no_test_uses_the_event_loop_fixture(
Expand Down
159 changes: 159 additions & 0 deletions tests/test_explicit_event_loop_fixture_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
from textwrap import dedent

from pytest import Pytester


def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import pytest
@pytest.mark.asyncio
async def test_coroutine_emits_warning(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_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_method(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import pytest
class TestEmitsWarning:
@pytest.mark.asyncio
async def test_coroutine_emits_warning(self, 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_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_staticmethod(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import pytest
class TestEmitsWarning:
@staticmethod
@pytest.mark.asyncio
async def test_coroutine_emits_warning(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_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_fixture(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import pytest
import pytest_asyncio
@pytest_asyncio.fixture
async def emits_warning(event_loop):
pass
@pytest.mark.asyncio
async def test_uses_fixture(emits_warning):
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_emit_warning_when_event_loop_is_explicitly_requested_in_async_gen_fixture(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import pytest
import pytest_asyncio
@pytest_asyncio.fixture
async def emits_warning(event_loop):
yield
@pytest.mark.asyncio
async def test_uses_fixture(emits_warning):
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_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_function(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import pytest
def test_uses_fixture(event_loop):
pass
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)


def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_fixture(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import pytest
@pytest.fixture
def any_fixture(event_loop):
pass
def test_uses_fixture(any_fixture):
pass
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1)
2 changes: 1 addition & 1 deletion tests/test_multiloop.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def event_loop():
@pytest.mark.asyncio
async def test_for_custom_loop(event_loop):
async def test_for_custom_loop():
"""This test should be executed using the custom loop."""
await asyncio.sleep(0.01)
assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"
Expand Down
Loading

0 comments on commit 044e568

Please sign in to comment.