Skip to content

Commit 270c24a

Browse files
authored
Merge pull request #12968 from arpitgupta-it/refactor/remove-is-generator
Remove _pytest.compat.is_generator() fixes #12960
2 parents fe60ceb + 70e41cb commit 270c24a

File tree

8 files changed

+63
-126
lines changed

8 files changed

+63
-126
lines changed

changelog/12960.breaking.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Test functions containing a yield now cause an explicit error. They have not been run since pytest 4.0, and were previously marked as an expected failure and deprecation warning.
2+
3+
See :ref:`the docs <yield tests deprecated>` for more information.

doc/en/deprecations.rst

+36-30
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,42 @@ an appropriate period of deprecation has passed.
374374

375375
Some breaking changes which could not be deprecated are also listed.
376376

377+
.. _yield tests deprecated:
378+
379+
``yield`` tests
380+
~~~~~~~~~~~~~~~
381+
382+
.. versionremoved:: 4.0
383+
384+
``yield`` tests ``xfail``.
385+
386+
.. versionremoved:: 8.4
387+
388+
``yield`` tests raise a collection error.
389+
390+
pytest no longer supports ``yield``-style tests, where a test function actually ``yield`` functions and values
391+
that are then turned into proper test methods. Example:
392+
393+
.. code-block:: python
394+
395+
def check(x, y):
396+
assert x**x == y
397+
398+
399+
def test_squared():
400+
yield check, 2, 4
401+
yield check, 3, 9
402+
403+
This would result in two actual test functions being generated.
404+
405+
This form of test function doesn't support fixtures properly, and users should switch to ``pytest.mark.parametrize``:
406+
407+
.. code-block:: python
408+
409+
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
410+
def test_squared(x, y):
411+
assert x**x == y
412+
377413
.. _nose-deprecation:
378414

379415
Support for tests written for nose
@@ -1270,36 +1306,6 @@ with the ``name`` parameter:
12701306
return cell()
12711307
12721308
1273-
.. _yield tests deprecated:
1274-
1275-
``yield`` tests
1276-
~~~~~~~~~~~~~~~
1277-
1278-
.. versionremoved:: 4.0
1279-
1280-
pytest supported ``yield``-style tests, where a test function actually ``yield`` functions and values
1281-
that are then turned into proper test methods. Example:
1282-
1283-
.. code-block:: python
1284-
1285-
def check(x, y):
1286-
assert x**x == y
1287-
1288-
1289-
def test_squared():
1290-
yield check, 2, 4
1291-
yield check, 3, 9
1292-
1293-
This would result into two actual test functions being generated.
1294-
1295-
This form of test function doesn't support fixtures properly, and users should switch to ``pytest.mark.parametrize``:
1296-
1297-
.. code-block:: python
1298-
1299-
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
1300-
def test_squared(x, y):
1301-
assert x**x == y
1302-
13031309
.. _internal classes accessed through node deprecated:
13041310

13051311
Internal classes accessed through ``Node``

src/_pytest/compat.py

-5
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,6 @@ class NotSetType(enum.Enum):
4343
# fmt: on
4444

4545

46-
def is_generator(func: object) -> bool:
47-
genfunc = inspect.isgeneratorfunction(func)
48-
return genfunc and not iscoroutinefunction(func)
49-
50-
5146
def iscoroutinefunction(func: object) -> bool:
5247
"""Return True if func is a coroutine function (a function defined with async
5348
def syntax, and doesn't contain yield), or a function decorated with

src/_pytest/fixtures.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
from _pytest.compat import getfuncargnames
5050
from _pytest.compat import getimfunc
5151
from _pytest.compat import getlocation
52-
from _pytest.compat import is_generator
5352
from _pytest.compat import NOTSET
5453
from _pytest.compat import NotSetType
5554
from _pytest.compat import safe_getattr
@@ -893,7 +892,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
893892
def call_fixture_func(
894893
fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
895894
) -> FixtureValue:
896-
if is_generator(fixturefunc):
895+
if inspect.isgeneratorfunction(fixturefunc):
897896
fixturefunc = cast(
898897
Callable[..., Generator[FixtureValue, None, None]], fixturefunc
899898
)

src/_pytest/python.py

+6-11
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
from _pytest.compat import get_real_func
4444
from _pytest.compat import getimfunc
4545
from _pytest.compat import is_async_function
46-
from _pytest.compat import is_generator
4746
from _pytest.compat import LEGACY_PATH
4847
from _pytest.compat import NOTSET
4948
from _pytest.compat import safe_getattr
@@ -57,7 +56,6 @@
5756
from _pytest.fixtures import FuncFixtureInfo
5857
from _pytest.fixtures import get_scope_node
5958
from _pytest.main import Session
60-
from _pytest.mark import MARK_GEN
6159
from _pytest.mark import ParameterSet
6260
from _pytest.mark.structures import get_unpacked_marks
6361
from _pytest.mark.structures import Mark
@@ -231,16 +229,13 @@ def pytest_pycollect_makeitem(
231229
lineno=lineno + 1,
232230
)
233231
elif getattr(obj, "__test__", True):
234-
if is_generator(obj):
235-
res = Function.from_parent(collector, name=name)
236-
reason = (
237-
f"yield tests were removed in pytest 4.0 - {name} will be ignored"
232+
if inspect.isgeneratorfunction(obj):
233+
fail(
234+
f"'yield' keyword is allowed in fixtures, but not in tests ({name})",
235+
pytrace=False,
238236
)
239-
res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
240-
res.warn(PytestCollectionWarning(reason))
241-
return res
242-
else:
243-
return list(collector._genfunctions(name, obj))
237+
return list(collector._genfunctions(name, obj))
238+
return None
244239
return None
245240

246241

testing/test_collection.py

+17
Original file line numberDiff line numberDiff line change
@@ -1878,3 +1878,20 @@ def test_respect_system_exceptions(
18781878
result.stdout.fnmatch_lines([f"*{head}*"])
18791879
result.stdout.fnmatch_lines([msg])
18801880
result.stdout.no_fnmatch_line(f"*{tail}*")
1881+
1882+
1883+
def test_yield_disallowed_in_tests(pytester: Pytester):
1884+
"""Ensure generator test functions with 'yield' fail collection (#12960)."""
1885+
pytester.makepyfile(
1886+
"""
1887+
def test_with_yield():
1888+
yield 1
1889+
"""
1890+
)
1891+
result = pytester.runpytest()
1892+
assert result.ret == 2
1893+
result.stdout.fnmatch_lines(
1894+
["*'yield' keyword is allowed in fixtures, but not in tests (test_with_yield)*"]
1895+
)
1896+
# Assert that no tests were collected
1897+
result.stdout.fnmatch_lines(["*collected 0 items*"])

testing/test_compat.py

-73
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,21 @@
55
from functools import cached_property
66
from functools import partial
77
from functools import wraps
8-
import sys
98
from typing import TYPE_CHECKING
109

1110
from _pytest.compat import _PytestWrapper
1211
from _pytest.compat import assert_never
1312
from _pytest.compat import get_real_func
14-
from _pytest.compat import is_generator
1513
from _pytest.compat import safe_getattr
1614
from _pytest.compat import safe_isclass
1715
from _pytest.outcomes import OutcomeException
18-
from _pytest.pytester import Pytester
1916
import pytest
2017

2118

2219
if TYPE_CHECKING:
2320
from typing_extensions import Literal
2421

2522

26-
def test_is_generator() -> None:
27-
def zap():
28-
yield # pragma: no cover
29-
30-
def foo():
31-
pass # pragma: no cover
32-
33-
assert is_generator(zap)
34-
assert not is_generator(foo)
35-
36-
3723
def test_real_func_loop_limit() -> None:
3824
class Evil:
3925
def __init__(self):
@@ -95,65 +81,6 @@ def foo(x):
9581
assert get_real_func(partial(foo)) is foo
9682

9783

98-
@pytest.mark.skipif(sys.version_info >= (3, 11), reason="coroutine removed")
99-
def test_is_generator_asyncio(pytester: Pytester) -> None:
100-
pytester.makepyfile(
101-
"""
102-
from _pytest.compat import is_generator
103-
import asyncio
104-
@asyncio.coroutine
105-
def baz():
106-
yield from [1,2,3]
107-
108-
def test_is_generator_asyncio():
109-
assert not is_generator(baz)
110-
"""
111-
)
112-
# avoid importing asyncio into pytest's own process,
113-
# which in turn imports logging (#8)
114-
result = pytester.runpytest_subprocess()
115-
result.stdout.fnmatch_lines(["*1 passed*"])
116-
117-
118-
def test_is_generator_async_syntax(pytester: Pytester) -> None:
119-
pytester.makepyfile(
120-
"""
121-
from _pytest.compat import is_generator
122-
def test_is_generator_py35():
123-
async def foo():
124-
await foo()
125-
126-
async def bar():
127-
pass
128-
129-
assert not is_generator(foo)
130-
assert not is_generator(bar)
131-
"""
132-
)
133-
result = pytester.runpytest()
134-
result.stdout.fnmatch_lines(["*1 passed*"])
135-
136-
137-
def test_is_generator_async_gen_syntax(pytester: Pytester) -> None:
138-
pytester.makepyfile(
139-
"""
140-
from _pytest.compat import is_generator
141-
def test_is_generator():
142-
async def foo():
143-
yield
144-
await foo()
145-
146-
async def bar():
147-
yield
148-
149-
assert not is_generator(foo)
150-
assert not is_generator(bar)
151-
"""
152-
)
153-
result = pytester.runpytest()
154-
result.stdout.fnmatch_lines(["*1 passed*"])
155-
156-
15784
class ErrorsHelper:
15885
@property
15986
def raise_baseexception(self):

testing/test_terminal.py

-5
Original file line numberDiff line numberDiff line change
@@ -1042,10 +1042,6 @@ def test_pass():
10421042
class TestClass(object):
10431043
def test_skip(self):
10441044
pytest.skip("hello")
1045-
def test_gen():
1046-
def check(x):
1047-
assert x == 1
1048-
yield check, 0
10491045
"""
10501046
)
10511047

@@ -1058,7 +1054,6 @@ def test_verbose_reporting(self, verbose_testfile, pytester: Pytester) -> None:
10581054
"*test_verbose_reporting.py::test_fail *FAIL*",
10591055
"*test_verbose_reporting.py::test_pass *PASS*",
10601056
"*test_verbose_reporting.py::TestClass::test_skip *SKIP*",
1061-
"*test_verbose_reporting.py::test_gen *XFAIL*",
10621057
]
10631058
)
10641059
assert result.ret == 1

0 commit comments

Comments
 (0)