Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
52ee148
Remove _pytest.compat.is_generator() fix #12960
Nov 16, 2024
16cab96
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 16, 2024
a44da2d
Added suggested changes
Nov 16, 2024
526529a
Remove _pytest.compat.is_generator() fix #12960
Nov 16, 2024
42cd296
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 16, 2024
38a2416
Added suggested changes
Nov 16, 2024
ecc26e0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 16, 2024
54782c9
Merge branch 'refactor/remove-is-generator' of https://github.com/arp…
Nov 16, 2024
3ab4a77
Removed stdlib tests as per review
Nov 16, 2024
362a217
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 16, 2024
78017f4
Pushed review changes
Nov 17, 2024
a074630
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 17, 2024
ec8723d
Merge branch 'main' into refactor/remove-is-generator
Nov 17, 2024
5398d7c
Add test for RuntimeError on 'yield' in tests
Nov 17, 2024
bad14ee
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 17, 2024
1bcaf2c
Rename changelog/12960.breaking.rst
Nov 17, 2024
54d8236
Merge branch 'main' into refactor/remove-is-generator
Nov 17, 2024
5d5739f
Merge branch 'pytest-dev:main' into refactor/remove-is-generator
Nov 18, 2024
3de3d0d
Added final review requested changes
Nov 18, 2024
7c24a0b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 18, 2024
1a5dbcb
Update changelog/12960.breaking.rst
nicoddemus Nov 18, 2024
70e41cb
Update doc/en/deprecations.rst
nicoddemus Nov 18, 2024
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/12960.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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.

See :ref:`the docs <yield tests deprecated>` for more information.
66 changes: 36 additions & 30 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,42 @@ an appropriate period of deprecation has passed.

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

.. _yield tests deprecated:

``yield`` tests
~~~~~~~~~~~~~~~

.. versionremoved:: 4.0

``yield`` tests ``xfail``.

.. versionremoved:: 8.4

``yield`` tests raise a collection error.

pytest no longer supports ``yield``-style tests, where a test function actually ``yield`` functions and values
that are then turned into proper test methods. Example:

.. code-block:: python

def check(x, y):
assert x**x == y


def test_squared():
yield check, 2, 4
yield check, 3, 9

This would result in two actual test functions being generated.

This form of test function doesn't support fixtures properly, and users should switch to ``pytest.mark.parametrize``:

.. code-block:: python

@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
def test_squared(x, y):
assert x**x == y

.. _nose-deprecation:

Support for tests written for nose
Expand Down Expand Up @@ -1270,36 +1306,6 @@ with the ``name`` parameter:
return cell()


.. _yield tests deprecated:

``yield`` tests
~~~~~~~~~~~~~~~

.. versionremoved:: 4.0

pytest supported ``yield``-style tests, where a test function actually ``yield`` functions and values
that are then turned into proper test methods. Example:

.. code-block:: python

def check(x, y):
assert x**x == y


def test_squared():
yield check, 2, 4
yield check, 3, 9

This would result into two actual test functions being generated.

This form of test function doesn't support fixtures properly, and users should switch to ``pytest.mark.parametrize``:

.. code-block:: python

@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
def test_squared(x, y):
assert x**x == y

.. _internal classes accessed through node deprecated:

Internal classes accessed through ``Node``
Expand Down
5 changes: 0 additions & 5 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ class NotSetType(enum.Enum):
# fmt: on


def is_generator(func: object) -> bool:
genfunc = inspect.isgeneratorfunction(func)
return genfunc and not iscoroutinefunction(func)


def iscoroutinefunction(func: object) -> bool:
"""Return True if func is a coroutine function (a function defined with async
def syntax, and doesn't contain yield), or a function decorated with
Expand Down
3 changes: 1 addition & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
from _pytest.compat import getfuncargnames
from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_generator
from _pytest.compat import NOTSET
from _pytest.compat import NotSetType
from _pytest.compat import safe_getattr
Expand Down Expand Up @@ -893,7 +892,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
def call_fixture_func(
fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
) -> FixtureValue:
if is_generator(fixturefunc):
if inspect.isgeneratorfunction(fixturefunc):
fixturefunc = cast(
Callable[..., Generator[FixtureValue, None, None]], fixturefunc
)
Expand Down
17 changes: 6 additions & 11 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
from _pytest.compat import is_async_function
from _pytest.compat import is_generator
from _pytest.compat import LEGACY_PATH
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
Expand All @@ -57,7 +56,6 @@
from _pytest.fixtures import FuncFixtureInfo
from _pytest.fixtures import get_scope_node
from _pytest.main import Session
from _pytest.mark import MARK_GEN
from _pytest.mark import ParameterSet
from _pytest.mark.structures import get_unpacked_marks
from _pytest.mark.structures import Mark
Expand Down Expand Up @@ -231,16 +229,13 @@
lineno=lineno + 1,
)
elif getattr(obj, "__test__", True):
if is_generator(obj):
res = Function.from_parent(collector, name=name)
reason = (
f"yield tests were removed in pytest 4.0 - {name} will be ignored"
if inspect.isgeneratorfunction(obj):
fail(

Check warning on line 233 in src/_pytest/python.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/python.py#L233

Added line #L233 was not covered by tests
f"'yield' keyword is allowed in fixtures, but not in tests ({name})",
pytrace=False,
)
res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
res.warn(PytestCollectionWarning(reason))
return res
else:
return list(collector._genfunctions(name, obj))
return list(collector._genfunctions(name, obj))
return None

Check warning on line 238 in src/_pytest/python.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/python.py#L238

Added line #L238 was not covered by tests
return None


Expand Down
17 changes: 17 additions & 0 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1878,3 +1878,20 @@ def test_respect_system_exceptions(
result.stdout.fnmatch_lines([f"*{head}*"])
result.stdout.fnmatch_lines([msg])
result.stdout.no_fnmatch_line(f"*{tail}*")


def test_yield_disallowed_in_tests(pytester: Pytester):
"""Ensure generator test functions with 'yield' fail collection (#12960)."""
pytester.makepyfile(
"""
def test_with_yield():
yield 1
"""
)
result = pytester.runpytest()
assert result.ret == 2
result.stdout.fnmatch_lines(
["*'yield' keyword is allowed in fixtures, but not in tests (test_with_yield)*"]
)
# Assert that no tests were collected
result.stdout.fnmatch_lines(["*collected 0 items*"])
73 changes: 0 additions & 73 deletions testing/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,21 @@
from functools import cached_property
from functools import partial
from functools import wraps
import sys
from typing import TYPE_CHECKING

from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never
from _pytest.compat import get_real_func
from _pytest.compat import is_generator
from _pytest.compat import safe_getattr
from _pytest.compat import safe_isclass
from _pytest.outcomes import OutcomeException
from _pytest.pytester import Pytester
import pytest


if TYPE_CHECKING:
from typing_extensions import Literal


def test_is_generator() -> None:
def zap():
yield # pragma: no cover

def foo():
pass # pragma: no cover

assert is_generator(zap)
assert not is_generator(foo)


def test_real_func_loop_limit() -> None:
class Evil:
def __init__(self):
Expand Down Expand Up @@ -95,65 +81,6 @@ def foo(x):
assert get_real_func(partial(foo)) is foo


@pytest.mark.skipif(sys.version_info >= (3, 11), reason="coroutine removed")
def test_is_generator_asyncio(pytester: Pytester) -> None:
pytester.makepyfile(
"""
from _pytest.compat import is_generator
import asyncio
@asyncio.coroutine
def baz():
yield from [1,2,3]

def test_is_generator_asyncio():
assert not is_generator(baz)
"""
)
# avoid importing asyncio into pytest's own process,
# which in turn imports logging (#8)
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 passed*"])


def test_is_generator_async_syntax(pytester: Pytester) -> None:
pytester.makepyfile(
"""
from _pytest.compat import is_generator
def test_is_generator_py35():
async def foo():
await foo()

async def bar():
pass

assert not is_generator(foo)
assert not is_generator(bar)
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"])


def test_is_generator_async_gen_syntax(pytester: Pytester) -> None:
pytester.makepyfile(
"""
from _pytest.compat import is_generator
def test_is_generator():
async def foo():
yield
await foo()

async def bar():
yield

assert not is_generator(foo)
assert not is_generator(bar)
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"])


class ErrorsHelper:
@property
def raise_baseexception(self):
Expand Down
5 changes: 0 additions & 5 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1042,10 +1042,6 @@ def test_pass():
class TestClass(object):
def test_skip(self):
pytest.skip("hello")
def test_gen():
def check(x):
assert x == 1
yield check, 0
"""
)

Expand All @@ -1058,7 +1054,6 @@ def test_verbose_reporting(self, verbose_testfile, pytester: Pytester) -> None:
"*test_verbose_reporting.py::test_fail *FAIL*",
"*test_verbose_reporting.py::test_pass *PASS*",
"*test_verbose_reporting.py::TestClass::test_skip *SKIP*",
"*test_verbose_reporting.py::test_gen *XFAIL*",
]
)
assert result.ret == 1
Expand Down
Loading