Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn when event loop fixture is requested explicitly in async functions #648

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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