Skip to content

Align behaviour for tests based on async generators with pytest #643

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

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
1 change: 1 addition & 0 deletions docs/source/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Changelog
- 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`.
- 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 <https://github.com/pytest-dev/pytest-asyncio/issues/642>`__
- Remove support for Python 3.7
- Declare support for Python 3.12

Expand Down
29 changes: 26 additions & 3 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
Item,
Metafunc,
Parser,
PytestCollectionWarning,
PytestPluginManager,
Session,
StashKey,
Expand Down Expand Up @@ -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"):
Expand All @@ -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
Expand Down
146 changes: 146 additions & 0 deletions tests/test_asyncio_mark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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
"""
)
)
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.*"]
)


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
"""
)
)
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*"]
)


def test_asyncio_mark_on_async_generator_function_emits_warning_in_auto_mode(
pytester: Pytester,
):
pytester.makepyfile(
dedent(
"""\
async def test_a():
yield
"""
)
)
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*"]
)


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
"""
)
)
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*"]
)


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
"""
)
)
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*"]
)


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
"""
)
)
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*"]
)


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
"""
)
)
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*"]
)
32 changes: 0 additions & 32 deletions tests/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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> *")
Expand Down