From 15678b651175b1a1619cf617cfa014d863a6199f Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 10:31:09 +0200 Subject: [PATCH 1/4] [refactor] Moved test for warning generated when sync functions are marked with "asyncio" into a separate test module. The "test_simple" module is named too general. Thus, it serves as a magnet for all kinds of tests that aren't connected to each other. This is one step to break up the "test_simple" module into more coherent test modules. Signed-off-by: Michael Seifert --- tests/test_asyncio_mark_on_sync_function.py | 33 +++++++++++++++++++++ tests/test_simple.py | 32 -------------------- 2 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 tests/test_asyncio_mark_on_sync_function.py diff --git a/tests/test_asyncio_mark_on_sync_function.py b/tests/test_asyncio_mark_on_sync_function.py new file mode 100644 index 00000000..7de7ec2f --- /dev/null +++ b/tests/test_asyncio_mark_on_sync_function.py @@ -0,0 +1,33 @@ +from textwrap import dedent + + +def test_warn_asyncio_marker_for_regular_func(testdir): + testdir.makepyfile( + dedent( + """\ + import pytest + + pytest_plugins = 'pytest_asyncio' + + @pytest.mark.asyncio + def test_a(): + pass + """ + ) + ) + testdir.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] + ) diff --git a/tests/test_simple.py b/tests/test_simple.py index 5e6a0d20..81fcd14b 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -300,38 +300,6 @@ async def test_no_warning_on_skip(): result.assert_outcomes(skipped=1) -def test_warn_asyncio_marker_for_regular_func(testdir): - testdir.makepyfile( - dedent( - """\ - import pytest - - pytest_plugins = 'pytest_asyncio' - - @pytest.mark.asyncio - def test_a(): - pass - """ - ) - ) - testdir.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = testdir.runpytest() - result.assert_outcomes(passed=1) - result.stdout.fnmatch_lines( - ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] - ) - - def test_invalid_asyncio_mode(testdir): result = testdir.runpytest("-o", "asyncio_mode=True") result.stderr.no_fnmatch_line("INTERNALERROR> *") From 5c0e4c5b009e79381761c4af10f979a1f4b7b6e4 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 21 Oct 2023 10:32:32 +0200 Subject: [PATCH 2/4] [refactor] test_warn_asyncio_marker_for_regular_func uses pytester, instead of the older "testdir" fixture. Signed-off-by: Michael Seifert --- tests/test_asyncio_mark_on_sync_function.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_asyncio_mark_on_sync_function.py b/tests/test_asyncio_mark_on_sync_function.py index 7de7ec2f..70152b47 100644 --- a/tests/test_asyncio_mark_on_sync_function.py +++ b/tests/test_asyncio_mark_on_sync_function.py @@ -1,8 +1,10 @@ from textwrap import dedent +from pytest import Pytester -def test_warn_asyncio_marker_for_regular_func(testdir): - testdir.makepyfile( + +def test_warn_asyncio_marker_for_regular_func(pytester: Pytester): + pytester.makepyfile( dedent( """\ import pytest @@ -15,7 +17,7 @@ def test_a(): """ ) ) - testdir.makefile( + pytester.makefile( ".ini", pytest=dedent( """\ @@ -26,7 +28,7 @@ def test_a(): """ ), ) - result = testdir.runpytest() + result = pytester.runpytest() result.assert_outcomes(passed=1) result.stdout.fnmatch_lines( ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] From 852cfbb88b71593788ed0b25d435fb1fb3bd87c6 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 23 Oct 2023 12:12:18 +0200 Subject: [PATCH 3/4] [feat] Test items based on asynchronous generators always exit with *xfail* status and emit a warning during the collection phase. This behavior is consistent with synchronous yield tests. Signed-off-by: Michael Seifert --- docs/source/reference/changelog.rst | 1 + pytest_asyncio/plugin.py | 29 ++- tests/test_asyncio_mark.py | 223 ++++++++++++++++++++ tests/test_asyncio_mark_on_sync_function.py | 35 --- 4 files changed, 250 insertions(+), 38 deletions(-) create mode 100644 tests/test_asyncio_mark.py delete mode 100644 tests/test_asyncio_mark_on_sync_function.py diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index fb7c5d00..7da71868 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -9,6 +9,7 @@ Changelog - 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`. +- Test items based on asynchronous generators always exit with *xfail* status and emit a warning during the collection phase. This behavior is consistent with synchronous yield tests. `#642 `__ - Remove support for Python 3.7 - Declare support for Python 3.12 diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f403ecb6..2babd96a 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -34,6 +34,7 @@ Item, Metafunc, Parser, + PytestCollectionWarning, PytestPluginManager, Session, StashKey, @@ -387,13 +388,13 @@ def _can_substitute(item: Function) -> bool: raise NotImplementedError() -class AsyncFunction(PytestAsyncioFunction): - """Pytest item that is a coroutine or an asynchronous generator""" +class Coroutine(PytestAsyncioFunction): + """Pytest item created by a coroutine""" @staticmethod def _can_substitute(item: Function) -> bool: func = item.obj - return _is_coroutine_or_asyncgen(func) + return asyncio.iscoroutinefunction(func) def runtest(self) -> None: if self.get_closest_marker("asyncio"): @@ -404,6 +405,28 @@ def runtest(self) -> None: super().runtest() +class AsyncGenerator(PytestAsyncioFunction): + """Pytest item created by an asynchronous generator""" + + @staticmethod + def _can_substitute(item: Function) -> bool: + func = item.obj + return inspect.isasyncgenfunction(func) + + @classmethod + def _from_function(cls, function: Function, /) -> Function: + async_gen_item = super()._from_function(function) + unsupported_item_type_message = ( + f"Tests based on asynchronous generators are not supported. " + f"{function.name} will be ignored." + ) + async_gen_item.warn(PytestCollectionWarning(unsupported_item_type_message)) + async_gen_item.add_marker( + pytest.mark.xfail(run=False, reason=unsupported_item_type_message) + ) + return async_gen_item + + class AsyncStaticMethod(PytestAsyncioFunction): """ Pytest item that is a coroutine or an asynchronous generator diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py new file mode 100644 index 00000000..65e54861 --- /dev/null +++ b/tests/test_asyncio_mark.py @@ -0,0 +1,223 @@ +from textwrap import dedent + +from pytest import Pytester + + +def test_asyncio_mark_on_sync_function_emits_warning(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio + def test_a(): + pass + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] + ) + + +def test_asyncio_mark_on_async_generator_function_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + @pytest.mark.asyncio + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_function_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_method_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + class TestAsyncGenerator: + @pytest.mark.asyncio + async def test_a(self): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_method_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + class TestAsyncGenerator: + @staticmethod + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_strict_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import pytest + + class TestAsyncGenerator: + @staticmethod + @pytest.mark.asyncio + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) + + +def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_auto_mode( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + class TestAsyncGenerator: + @staticmethod + async def test_a(): + yield + """ + ) + ) + pytester.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = auto + filterwarnings = + default + """ + ), + ) + result = pytester.runpytest() + result.assert_outcomes(xfailed=1, warnings=1) + result.stdout.fnmatch_lines( + ["*Tests based on asynchronous generators are not supported*"] + ) diff --git a/tests/test_asyncio_mark_on_sync_function.py b/tests/test_asyncio_mark_on_sync_function.py deleted file mode 100644 index 70152b47..00000000 --- a/tests/test_asyncio_mark_on_sync_function.py +++ /dev/null @@ -1,35 +0,0 @@ -from textwrap import dedent - -from pytest import Pytester - - -def test_warn_asyncio_marker_for_regular_func(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import pytest - - pytest_plugins = 'pytest_asyncio' - - @pytest.mark.asyncio - def test_a(): - pass - """ - ) - ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() - result.assert_outcomes(passed=1) - result.stdout.fnmatch_lines( - ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] - ) From 861a8ae85d768fb54a91c45cc5ecb7bff5cdc117 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Mon, 23 Oct 2023 12:32:04 +0200 Subject: [PATCH 4/4] [refactor] Simplified code in test_asyncio_mark. Tests use command-line arguments to set the asyncio mode and warnings filter, instead of a .ini file. This reduces the number of lines in the test module significantly. Signed-off-by: Michael Seifert --- tests/test_asyncio_mark.py | 91 +++----------------------------------- 1 file changed, 7 insertions(+), 84 deletions(-) diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py index 65e54861..b514cbcd 100644 --- a/tests/test_asyncio_mark.py +++ b/tests/test_asyncio_mark.py @@ -15,18 +15,7 @@ def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(passed=1) result.stdout.fnmatch_lines( ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] @@ -47,18 +36,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -76,18 +54,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = auto - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -109,18 +76,7 @@ async def test_a(self): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -140,18 +96,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = auto - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -174,18 +119,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = strict - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=strict", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"] @@ -205,18 +139,7 @@ async def test_a(): """ ) ) - pytester.makefile( - ".ini", - pytest=dedent( - """\ - [pytest] - asyncio_mode = auto - filterwarnings = - default - """ - ), - ) - result = pytester.runpytest() + result = pytester.runpytest("--asyncio-mode=auto", "-W default") result.assert_outcomes(xfailed=1, warnings=1) result.stdout.fnmatch_lines( ["*Tests based on asynchronous generators are not supported*"]