Skip to content

Commit 4bca1d7

Browse files
committed
[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 <m.seifert@digitalernachschub.de>
1 parent d697a12 commit 4bca1d7

File tree

4 files changed

+250
-38
lines changed

4 files changed

+250
-38
lines changed

docs/source/reference/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Changelog
99
- Deprecate redefinition of the `event_loop` fixture. `#587 <https://github.com/pytest-dev/pytest-asyncio/issues/531>`_
1010
Users requiring a class-scoped or module-scoped asyncio event loop for their tests
1111
should mark the corresponding class or module with `asyncio_event_loop`.
12+
- 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>`__
1213
- Remove support for Python 3.7
1314
- Declare support for Python 3.12
1415

pytest_asyncio/plugin.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
Item,
3535
Metafunc,
3636
Parser,
37+
PytestCollectionWarning,
3738
PytestPluginManager,
3839
Session,
3940
StashKey,
@@ -387,13 +388,13 @@ def _can_substitute(item: Function) -> bool:
387388
raise NotImplementedError()
388389

389390

390-
class AsyncFunction(PytestAsyncioFunction):
391-
"""Pytest item that is a coroutine or an asynchronous generator"""
391+
class Coroutine(PytestAsyncioFunction):
392+
"""Pytest item created by a coroutine"""
392393

393394
@staticmethod
394395
def _can_substitute(item: Function) -> bool:
395396
func = item.obj
396-
return _is_coroutine_or_asyncgen(func)
397+
return asyncio.iscoroutinefunction(func)
397398

398399
def runtest(self) -> None:
399400
if self.get_closest_marker("asyncio"):
@@ -404,6 +405,28 @@ def runtest(self) -> None:
404405
super().runtest()
405406

406407

408+
class AsyncGenerator(PytestAsyncioFunction):
409+
"""Pytest item created by an asynchronous generator"""
410+
411+
@staticmethod
412+
def _can_substitute(item: Function) -> bool:
413+
func = item.obj
414+
return inspect.isasyncgenfunction(func)
415+
416+
@classmethod
417+
def _from_function(cls, function: Function, /) -> Function:
418+
async_gen_item = super()._from_function(function)
419+
unsupported_item_type_message = (
420+
f"Tests based on asynchronous generators are not supported. "
421+
f"{function.name} will be ignored."
422+
)
423+
async_gen_item.warn(PytestCollectionWarning(unsupported_item_type_message))
424+
async_gen_item.add_marker(
425+
pytest.mark.xfail(run=False, reason=unsupported_item_type_message)
426+
)
427+
return async_gen_item
428+
429+
407430
class AsyncStaticMethod(PytestAsyncioFunction):
408431
"""
409432
Pytest item that is a coroutine or an asynchronous generator

tests/test_asyncio_mark.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
from textwrap import dedent
2+
3+
from pytest import Pytester
4+
5+
6+
def test_asyncio_mark_on_sync_function_emits_warning(pytester: Pytester):
7+
pytester.makepyfile(
8+
dedent(
9+
"""\
10+
import pytest
11+
12+
@pytest.mark.asyncio
13+
def test_a():
14+
pass
15+
"""
16+
)
17+
)
18+
pytester.makefile(
19+
".ini",
20+
pytest=dedent(
21+
"""\
22+
[pytest]
23+
asyncio_mode = strict
24+
filterwarnings =
25+
default
26+
"""
27+
),
28+
)
29+
result = pytester.runpytest()
30+
result.assert_outcomes(passed=1)
31+
result.stdout.fnmatch_lines(
32+
["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"]
33+
)
34+
35+
36+
def test_asyncio_mark_on_async_generator_function_emits_warning_in_strict_mode(
37+
pytester: Pytester,
38+
):
39+
pytester.makepyfile(
40+
dedent(
41+
"""\
42+
import pytest
43+
44+
@pytest.mark.asyncio
45+
async def test_a():
46+
yield
47+
"""
48+
)
49+
)
50+
pytester.makefile(
51+
".ini",
52+
pytest=dedent(
53+
"""\
54+
[pytest]
55+
asyncio_mode = strict
56+
filterwarnings =
57+
default
58+
"""
59+
),
60+
)
61+
result = pytester.runpytest()
62+
result.assert_outcomes(xfailed=1, warnings=1)
63+
result.stdout.fnmatch_lines(
64+
["*Tests based on asynchronous generators are not supported*"]
65+
)
66+
67+
68+
def test_asyncio_mark_on_async_generator_function_emits_warning_in_auto_mode(
69+
pytester: Pytester,
70+
):
71+
pytester.makepyfile(
72+
dedent(
73+
"""\
74+
async def test_a():
75+
yield
76+
"""
77+
)
78+
)
79+
pytester.makefile(
80+
".ini",
81+
pytest=dedent(
82+
"""\
83+
[pytest]
84+
asyncio_mode = auto
85+
filterwarnings =
86+
default
87+
"""
88+
),
89+
)
90+
result = pytester.runpytest()
91+
result.assert_outcomes(xfailed=1, warnings=1)
92+
result.stdout.fnmatch_lines(
93+
["*Tests based on asynchronous generators are not supported*"]
94+
)
95+
96+
97+
def test_asyncio_mark_on_async_generator_method_emits_warning_in_strict_mode(
98+
pytester: Pytester,
99+
):
100+
pytester.makepyfile(
101+
dedent(
102+
"""\
103+
import pytest
104+
105+
class TestAsyncGenerator:
106+
@pytest.mark.asyncio
107+
async def test_a(self):
108+
yield
109+
"""
110+
)
111+
)
112+
pytester.makefile(
113+
".ini",
114+
pytest=dedent(
115+
"""\
116+
[pytest]
117+
asyncio_mode = strict
118+
filterwarnings =
119+
default
120+
"""
121+
),
122+
)
123+
result = pytester.runpytest()
124+
result.assert_outcomes(xfailed=1, warnings=1)
125+
result.stdout.fnmatch_lines(
126+
["*Tests based on asynchronous generators are not supported*"]
127+
)
128+
129+
130+
def test_asyncio_mark_on_async_generator_method_emits_warning_in_auto_mode(
131+
pytester: Pytester,
132+
):
133+
pytester.makepyfile(
134+
dedent(
135+
"""\
136+
class TestAsyncGenerator:
137+
@staticmethod
138+
async def test_a():
139+
yield
140+
"""
141+
)
142+
)
143+
pytester.makefile(
144+
".ini",
145+
pytest=dedent(
146+
"""\
147+
[pytest]
148+
asyncio_mode = auto
149+
filterwarnings =
150+
default
151+
"""
152+
),
153+
)
154+
result = pytester.runpytest()
155+
result.assert_outcomes(xfailed=1, warnings=1)
156+
result.stdout.fnmatch_lines(
157+
["*Tests based on asynchronous generators are not supported*"]
158+
)
159+
160+
161+
def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_strict_mode(
162+
pytester: Pytester,
163+
):
164+
pytester.makepyfile(
165+
dedent(
166+
"""\
167+
import pytest
168+
169+
class TestAsyncGenerator:
170+
@staticmethod
171+
@pytest.mark.asyncio
172+
async def test_a():
173+
yield
174+
"""
175+
)
176+
)
177+
pytester.makefile(
178+
".ini",
179+
pytest=dedent(
180+
"""\
181+
[pytest]
182+
asyncio_mode = strict
183+
filterwarnings =
184+
default
185+
"""
186+
),
187+
)
188+
result = pytester.runpytest()
189+
result.assert_outcomes(xfailed=1, warnings=1)
190+
result.stdout.fnmatch_lines(
191+
["*Tests based on asynchronous generators are not supported*"]
192+
)
193+
194+
195+
def test_asyncio_mark_on_async_generator_staticmethod_emits_warning_in_auto_mode(
196+
pytester: Pytester,
197+
):
198+
pytester.makepyfile(
199+
dedent(
200+
"""\
201+
class TestAsyncGenerator:
202+
@staticmethod
203+
async def test_a():
204+
yield
205+
"""
206+
)
207+
)
208+
pytester.makefile(
209+
".ini",
210+
pytest=dedent(
211+
"""\
212+
[pytest]
213+
asyncio_mode = auto
214+
filterwarnings =
215+
default
216+
"""
217+
),
218+
)
219+
result = pytester.runpytest()
220+
result.assert_outcomes(xfailed=1, warnings=1)
221+
result.stdout.fnmatch_lines(
222+
["*Tests based on asynchronous generators are not supported*"]
223+
)

tests/test_asyncio_mark_on_sync_function.py

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)