diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 13c5080b..fb7c5d00 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -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 `_ +- Deprecate redefinition of the `event_loop` fixture. `#587 `_ + 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) =================== diff --git a/docs/source/reference/fixtures.rst b/docs/source/reference/fixtures.rst index adcc092d..d5032ba9 100644 --- a/docs/source/reference/fixtures.rst +++ b/docs/source/reference/fixtures.rst @@ -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. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 7d41779f..3dba6c79 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -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( @@ -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 @@ -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() diff --git a/setup.cfg b/setup.cfg index 5dd6a63e..7b7a1b6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py index b676df2d..07b76501 100644 --- a/tests/test_event_loop_fixture_finalizer.py +++ b/tests/test_event_loop_fixture_finalizer.py @@ -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*") @@ -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*") diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py new file mode 100644 index 00000000..b23ff642 --- /dev/null +++ b/tests/test_event_loop_fixture_override_deprecation.py @@ -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) diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py index 6c47d68c..86a88eec 100644 --- a/tests/test_multiloop.py +++ b/tests/test_multiloop.py @@ -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" @@ -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)