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

Deprecate event loop fixture overrides #632

Merged
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
7 changes: 5 additions & 2 deletions docs/source/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ Changelog

1.0.0 (UNRELEASED)
==================
- Remove support for Python 3.7
- Declare support for Python 3.12
- Class-scoped and module-scoped event loops can be requested
via the _asyncio_event_loop_ mark. `#620 <https://github.com/pytest-dev/pytest-asyncio/pull/620>`_
- Deprecate redefinition of the `event_loop` fixture. `#587 <https://github.com/pytest-dev/pytest-asyncio/issues/531>`_
Users requiring a class-scoped or module-scoped asyncio event loop for their tests
should mark the corresponding class or module with `asyncio_event_loop`.
- Remove support for Python 3.7
- Declare support for Python 3.12

0.21.1 (2023-07-12)
===================
Expand Down
18 changes: 1 addition & 17 deletions docs/source/reference/fixtures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,7 @@ to ``function`` scope.
Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The `asyncio <#pytest-mark-asyncio>`__ marker
is used to mark coroutines that should be treated as test functions.

The ``event_loop`` fixture can be overridden in any of the standard pytest locations,
e.g. directly in the test file, or in ``conftest.py``. This allows redefining the
fixture scope, for example:

.. code-block:: python

@pytest.fixture(scope="module")
def event_loop():
policy = asyncio.get_event_loop_policy()
loop = policy.new_event_loop()
yield loop
loop.close()

When defining multiple ``event_loop`` fixtures, you should ensure that their scopes don't overlap.
Each of the fixtures replace the running event loop, potentially without proper clean up.
This will emit a warning and likely lead to errors in your tests suite.
You can manually check for overlapping ``event_loop`` fixtures by running pytest with the ``--setup-show`` option.
If your tests require an asyncio event loop with class or module scope, apply the `asyncio_event_loop mark <./markers.html/#pytest-mark-asyncio-event-loop>`__ to the respective class or module.

If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture.

Expand Down
29 changes: 29 additions & 0 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,18 @@ def _hypothesis_test_wraps_coroutine(function: Any) -> bool:
return _is_coroutine(function.hypothesis.inner_test)


_REDEFINED_EVENT_LOOP_FIXTURE_WARNING = dedent(
"""\
The event_loop fixture provided by pytest-asyncio has been redefined in
%s:%d
Replacing the event_loop fixture with a custom implementation is deprecated
and will lead to errors in the future.
If you want to request an asyncio event loop with a class or module scope,
please attach the asyncio_event_loop mark to the respective class or module.
"""
)


@pytest.hookimpl(tryfirst=True)
def pytest_generate_tests(metafunc: Metafunc) -> None:
for event_loop_provider_node, _ in metafunc.definition.iter_markers_with_node(
Expand Down Expand Up @@ -497,6 +509,17 @@ def pytest_fixture_setup(
) -> Optional[object]:
"""Adjust the event loop policy when an event loop is produced."""
if fixturedef.argname == "event_loop":
# FixtureDef.baseid is an empty string when the Fixture was found in a plugin.
# This is also true, when the fixture was defined in a conftest.py
# at the rootdir.
fixture_filename = inspect.getsourcefile(fixturedef.func)
if not getattr(fixturedef.func, "__original_func", False):
_, fixture_line_number = inspect.getsourcelines(fixturedef.func)
warnings.warn(
_REDEFINED_EVENT_LOOP_FIXTURE_WARNING
% (fixture_filename, fixture_line_number),
DeprecationWarning,
)
# The use of a fixture finalizer is preferred over the
# pytest_fixture_post_finalizer hook. The fixture finalizer is invoked once
# for each fixture, whereas the hook may be invoked multiple times for
Expand Down Expand Up @@ -691,6 +714,12 @@ 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."""
# Add a magic value to the fixture function, so that we can check for overrides
# of this fixture in pytest_fixture_setup
# The magic value must be part of the function definition, because pytest may have
# multiple instances of the fixture function
event_loop.__original_func = True

loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ asyncio_mode = auto
junit_family=xunit2
filterwarnings =
error
ignore:The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning

[flake8]
max-line-length = 88
4 changes: 2 additions & 2 deletions tests/test_event_loop_fixture_finalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async def test_ends_with_unclosed_loop():
)
)
result = pytester.runpytest("--asyncio-mode=strict", "-W", "default")
result.assert_outcomes(passed=1, warnings=1)
result.assert_outcomes(passed=1, warnings=2)
result.stdout.fnmatch_lines("*unclosed event loop*")


Expand All @@ -133,5 +133,5 @@ async def test_ends_with_unclosed_loop():
)
)
result = pytester.runpytest("--asyncio-mode=strict", "-W", "default")
result.assert_outcomes(passed=1, warnings=1)
result.assert_outcomes(passed=1, warnings=2)
result.stdout.fnmatch_lines("*unclosed event loop*")
81 changes: 81 additions & 0 deletions tests/test_event_loop_fixture_override_deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from textwrap import dedent

from pytest import Pytester


def test_emit_warning_when_event_loop_fixture_is_redefined(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():
pass

@pytest.mark.asyncio
async def test_emits_warning_when_referenced_explicitly(event_loop):
pass
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=2, warnings=2)


def test_does_not_emit_warning_when_no_test_uses_the_event_loop_fixture(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest

@pytest.fixture
def event_loop():
loop = asyncio.new_event_loop()
yield loop
loop.close()

def test_emits_no_warning():
pass
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1, warnings=0)


def test_emit_warning_when_redefined_event_loop_is_used_by_fixture(pytester: Pytester):
pytester.makepyfile(
dedent(
"""\
import asyncio
import pytest
import pytest_asyncio

@pytest.fixture
def event_loop():
loop = asyncio.new_event_loop()
yield loop
loop.close()

@pytest_asyncio.fixture
async def uses_event_loop():
pass

def test_emits_warning(uses_event_loop):
pass
"""
)
)
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=1, warnings=1)
4 changes: 2 additions & 2 deletions 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():
async def test_for_custom_loop(event_loop):
"""This test should be executed using the custom loop."""
await asyncio.sleep(0.01)
assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"
Expand All @@ -67,4 +67,4 @@ async def test_dependent_fixture(dependent_fixture):
)
)
result = pytester.runpytest_subprocess("--asyncio-mode=strict")
result.assert_outcomes(passed=2)
result.assert_outcomes(passed=2, warnings=2)