diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst index eb08bae6..e774791e 100644 --- a/docs/source/concepts.rst +++ b/docs/source/concepts.rst @@ -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 ==================== diff --git a/docs/source/reference/fixtures/index.rst b/docs/source/reference/fixtures/index.rst index 98fe5382..c0bfd300 100644 --- a/docs/source/reference/fixtures/index.rst +++ b/docs/source/reference/fixtures/index.rst @@ -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 `__. +is available as the return value of this fixture for synchronous functions, or via `asyncio.get_running_loop `__ for asynchronous functions. The event loop is closed when the fixture scope ends. The fixture scope defaults to ``function`` scope. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index b58197e4..18c86869 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -35,6 +35,7 @@ Metafunc, Parser, PytestCollectionWarning, + PytestDeprecationWarning, PytestPluginManager, Session, StashKey, @@ -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) @@ -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), @@ -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: diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py index 2e72d5de..aa2ce3d7 100644 --- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py +++ b/tests/async_fixtures/test_async_fixtures_with_finalizer.py @@ -26,14 +26,14 @@ 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)) @@ -41,7 +41,7 @@ async def port_afinalizer(): @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 diff --git a/tests/async_fixtures/test_nested.py b/tests/async_fixtures/test_nested.py index e81e7824..da7ee3a1 100644 --- a/tests/async_fixtures/test_nested.py +++ b/tests/async_fixtures/test_nested.py @@ -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 diff --git a/tests/markers/test_class_marker.py b/tests/markers/test_class_marker.py index 68425575..f8cf4ca0 100644 --- a/tests/markers/test_class_marker.py +++ b/tests/markers/test_class_marker.py @@ -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 diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py index b23ff642..3484ef76 100644 --- a/tests/test_event_loop_fixture_override_deprecation.py +++ b/tests/test_event_loop_fixture_override_deprecation.py @@ -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( diff --git a/tests/test_explicit_event_loop_fixture_request.py b/tests/test_explicit_event_loop_fixture_request.py new file mode 100644 index 00000000..8c4b732c --- /dev/null +++ b/tests/test_explicit_event_loop_fixture_request.py @@ -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) diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py index 86a88eec..c3713cc9 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(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" diff --git a/tests/test_simple.py b/tests/test_simple.py index 81fcd14b..b6020c69 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -70,7 +70,7 @@ async def test_asyncio_marker_with_default_param(a_param=None): @pytest.mark.asyncio -async def test_unused_port_fixture(unused_tcp_port, event_loop): +async def test_unused_port_fixture(unused_tcp_port): """Test the unused TCP port fixture.""" async def closer(_, writer): @@ -86,7 +86,7 @@ async def closer(_, writer): @pytest.mark.asyncio -async def test_unused_udp_port_fixture(unused_udp_port, event_loop): +async def test_unused_udp_port_fixture(unused_udp_port): """Test the unused TCP port fixture.""" class Closer: @@ -96,6 +96,7 @@ def connection_made(self, transport): def connection_lost(self, *arg, **kwd): pass + event_loop = asyncio.get_running_loop() transport1, _ = await event_loop.create_datagram_endpoint( Closer, local_addr=("127.0.0.1", unused_udp_port), @@ -113,7 +114,7 @@ def connection_lost(self, *arg, **kwd): @pytest.mark.asyncio -async def test_unused_port_factory_fixture(unused_tcp_port_factory, event_loop): +async def test_unused_port_factory_fixture(unused_tcp_port_factory): """Test the unused TCP port factory fixture.""" async def closer(_, writer): @@ -142,7 +143,7 @@ async def closer(_, writer): @pytest.mark.asyncio -async def test_unused_udp_port_factory_fixture(unused_udp_port_factory, event_loop): +async def test_unused_udp_port_factory_fixture(unused_udp_port_factory): """Test the unused UDP port factory fixture.""" class Closer: @@ -158,6 +159,7 @@ def connection_lost(self, *arg, **kwd): unused_udp_port_factory(), ) + event_loop = asyncio.get_running_loop() transport1, _ = await event_loop.create_datagram_endpoint( Closer, local_addr=("127.0.0.1", port1), @@ -228,13 +230,6 @@ def mock_unused_udp_port(_ignored): class TestMarkerInClassBasedTests: """Test that asyncio marked functions work for methods of test classes.""" - @pytest.mark.asyncio - async def test_asyncio_marker_with_explicit_loop_fixture(self, event_loop): - """Test the "asyncio" marker works on a method in - a class-based test with explicit loop fixture.""" - ret = await async_coro() - assert ret == "ok" - @pytest.mark.asyncio async def test_asyncio_marker_with_implicit_loop_fixture(self): """Test the "asyncio" marker works on a method in @@ -257,11 +252,11 @@ async def test_no_event_loop(self, loop): assert await loop.run_in_executor(None, self.foo) == 1 @pytest.mark.asyncio - async def test_event_loop_after_fixture(self, loop, event_loop): + async def test_event_loop_after_fixture(self, loop): assert await loop.run_in_executor(None, self.foo) == 1 @pytest.mark.asyncio - async def test_event_loop_before_fixture(self, event_loop, loop): + async def test_event_loop_before_fixture(self, loop): assert await loop.run_in_executor(None, self.foo) == 1 diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index 8f1caee5..14d3498a 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -16,7 +16,7 @@ def event_loop(): @pytest.mark.asyncio -async def test_subprocess(event_loop): +async def test_subprocess(): """Starting a subprocess should be possible.""" proc = await asyncio.subprocess.create_subprocess_exec( sys.executable, "--version", stdout=asyncio.subprocess.PIPE