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

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