From 59a31cac21ac65885745d9b8612c286192f6dd40 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 22 Sep 2025 14:56:09 +0200 Subject: [PATCH] fix #13564 - warn when fixtures get wrapped with a decorator --- .gitignore | 1 + changelog/13564.improvement.rst | 2 ++ src/_pytest/fixtures.py | 36 +++++++++++++++++++++++++- testing/python/fixtures.py | 46 +++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 changelog/13564.improvement.rst diff --git a/.gitignore b/.gitignore index c4557b33a1c..59cdcc6d968 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ pip-wheel-metadata/ # pytest debug logs generated via --debug pytestdebug.log +.claude/settings.local.json diff --git a/changelog/13564.improvement.rst b/changelog/13564.improvement.rst new file mode 100644 index 00000000000..c1704e19dd3 --- /dev/null +++ b/changelog/13564.improvement.rst @@ -0,0 +1,2 @@ +Issue a warning when fixtures are wrapped with a decorator, as that excludes +them from being discovered safely by pytest. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 91f1b3a67f6..5b77f6ad995 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1271,7 +1271,9 @@ def __init__( def __repr__(self) -> str: return f"" - def __get__(self, instance, owner=None): + def __get__( + self, instance: object, owner: type | None = None + ) -> FixtureFunctionDefinition: """Behave like a method if the function it was applied to was a method.""" return FixtureFunctionDefinition( function=self._fixture_function, @@ -1765,6 +1767,35 @@ def _register_fixture( if autouse: self._nodeid_autousenames.setdefault(nodeid or "", []).append(name) + def _check_for_wrapped_fixture( + self, holder: object, name: str, obj: object, nodeid: str | None + ) -> None: + """Check if an object might be a fixture wrapped in decorators and warn if so.""" + # Only check objects that are not None and not already FixtureFunctionDefinition + if obj is None: + return + try: + maybe_def = get_real_func(obj) + except Exception: + warnings.warn( + f"could not get real function for fixture {name} on {holder}", + stacklevel=2, + ) + else: + if isinstance(maybe_def, FixtureFunctionDefinition): + fixture_func = maybe_def._get_wrapped_function() + self._issue_fixture_wrapped_warning(name, nodeid, fixture_func) + + def _issue_fixture_wrapped_warning( + self, fixture_name: str, nodeid: str | None, fixture_func: Any + ) -> None: + """Issue a warning about a fixture that cannot be discovered due to decorators.""" + from _pytest.warning_types import PytestWarning + from _pytest.warning_types import warn_explicit_for + + msg = f"cannot discover {fixture_name} due to being wrapped in decorators" + warn_explicit_for(fixture_func, PytestWarning(msg)) + @overload def parsefactories( self, @@ -1845,6 +1876,9 @@ def parsefactories( ids=marker.ids, autouse=marker.autouse, ) + else: + # Check if this might be a wrapped fixture that we can't discover + self._check_for_wrapped_fixture(holderobj, name, obj_ub, nodeid) def getfixturedefs( self, argname: str, node: nodes.Node diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8b97d35c21e..164e8364a47 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -5069,3 +5069,49 @@ def test_method(self, /, fix): ) result = pytester.runpytest() result.assert_outcomes(passed=1) + + +@pytest.mark.filterwarnings( + "default:cannot discover * due to being wrapped in decorators:pytest.PytestWarning" +) +def test_custom_decorated_fixture_warning(pytester: Pytester) -> None: + """ + Test that fixtures decorated with custom decorators using functools.wraps + generate a warning about not being discoverable. + """ + pytester.makepyfile( + """ + import pytest + import functools + + def custom_deco(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + + class TestClass: + @custom_deco + @pytest.fixture + def my_fixture(self): + return "fixture_value" + + def test_fixture_usage(self, my_fixture): + assert my_fixture == "fixture_value" + """ + ) + result = pytester.runpytest_inprocess( + "-v", "-rw", "-W", "default::pytest.PytestWarning" + ) + + # Should get a warning about the decorated fixture during collection with correct location + result.stdout.fnmatch_lines( + [ + "*test_custom_decorated_fixture_warning.py:*: " + "PytestWarning: cannot discover my_fixture due to being wrapped in decorators*" + ] + ) + + # The test should fail because fixture is not found + result.stdout.fnmatch_lines(["*fixture 'my_fixture' not found*"]) + result.assert_outcomes(errors=1)