Skip to content

Commit 9e7e132

Browse files
fix #13564 - warn when fixtures get wrapped with a decorator
1 parent 9d7bf4e commit 9e7e132

File tree

5 files changed

+87
-2
lines changed

5 files changed

+87
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ pip-wheel-metadata/
5757

5858
# pytest debug logs generated via --debug
5959
pytestdebug.log
60+
.claude/settings.local.json

changelog/13564.improvement.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Issue a warning when fixtures are wrapped with a decorator, as that excludes
2+
them from being discovered safely by pytest.

src/_pytest/fixtures.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1253,7 +1253,9 @@ def __init__(
12531253
def __repr__(self) -> str:
12541254
return f"<pytest_fixture({self._fixture_function})>"
12551255

1256-
def __get__(self, instance, owner=None):
1256+
def __get__(
1257+
self, instance: object, owner: type | None = None
1258+
) -> FixtureFunctionDefinition:
12571259
"""Behave like a method if the function it was applied to was a method."""
12581260
return FixtureFunctionDefinition(
12591261
function=self._fixture_function,
@@ -1747,6 +1749,35 @@ def _register_fixture(
17471749
if autouse:
17481750
self._nodeid_autousenames.setdefault(nodeid or "", []).append(name)
17491751

1752+
def _check_for_wrapped_fixture(
1753+
self, holder: object, name: str, obj: object, nodeid: str | None
1754+
) -> None:
1755+
"""Check if an object might be a fixture wrapped in decorators and warn if so."""
1756+
# Only check objects that are not None and not already FixtureFunctionDefinition
1757+
if obj is None:
1758+
return
1759+
try:
1760+
maybe_def = get_real_func(obj)
1761+
except Exception:
1762+
warnings.warn(
1763+
f"could not get real function for fixture {name} on {holder}",
1764+
stacklevel=2,
1765+
)
1766+
else:
1767+
if isinstance(maybe_def, FixtureFunctionDefinition):
1768+
fixture_func = maybe_def._get_wrapped_function()
1769+
self._issue_fixture_wrapped_warning(name, nodeid, fixture_func)
1770+
1771+
def _issue_fixture_wrapped_warning(
1772+
self, fixture_name: str, nodeid: str | None, fixture_func: Any
1773+
) -> None:
1774+
"""Issue a warning about a fixture that cannot be discovered due to decorators."""
1775+
from _pytest.warning_types import PytestWarning
1776+
from _pytest.warning_types import warn_explicit_for
1777+
1778+
msg = f"cannot discover {fixture_name} due to being wrapped in decorators"
1779+
warn_explicit_for(fixture_func, PytestWarning(msg))
1780+
17501781
@overload
17511782
def parsefactories(
17521783
self,
@@ -1827,6 +1858,9 @@ def parsefactories(
18271858
ids=marker.ids,
18281859
autouse=marker.autouse,
18291860
)
1861+
else:
1862+
# Check if this might be a wrapped fixture that we can't discover
1863+
self._check_for_wrapped_fixture(holderobj, name, obj_ub, nodeid)
18301864

18311865
def getfixturedefs(
18321866
self, argname: str, node: nodes.Node

src/_pytest/monkeypatch.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,9 @@ def syspath_prepend(self, path) -> None:
346346
# https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
347347
# this is only needed when pkg_resources was already loaded by the namespace package
348348
if "pkg_resources" in sys.modules:
349-
from pkg_resources import fixup_namespace_packages
349+
from pkg_resources import ( # type: ignore[import-untyped]
350+
fixup_namespace_packages,
351+
)
350352

351353
fixup_namespace_packages(str(path))
352354

testing/python/fixtures.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5033,3 +5033,49 @@ def test_foo(another_fixture):
50335033
)
50345034
result = pytester.runpytest()
50355035
result.assert_outcomes(passed=1)
5036+
5037+
5038+
@pytest.mark.filterwarnings(
5039+
"default:cannot discover * due to being wrapped in decorators:pytest.PytestWarning"
5040+
)
5041+
def test_custom_decorated_fixture_warning(pytester: Pytester) -> None:
5042+
"""
5043+
Test that fixtures decorated with custom decorators using functools.wraps
5044+
generate a warning about not being discoverable.
5045+
"""
5046+
pytester.makepyfile(
5047+
"""
5048+
import pytest
5049+
import functools
5050+
5051+
def custom_deco(func):
5052+
@functools.wraps(func)
5053+
def wrapper(*args, **kwargs):
5054+
return func(*args, **kwargs)
5055+
return wrapper
5056+
5057+
class TestClass:
5058+
@custom_deco
5059+
@pytest.fixture
5060+
def my_fixture(self):
5061+
return "fixture_value"
5062+
5063+
def test_fixture_usage(self, my_fixture):
5064+
assert my_fixture == "fixture_value"
5065+
"""
5066+
)
5067+
result = pytester.runpytest_inprocess(
5068+
"-v", "-rw", "-W", "default::pytest.PytestWarning"
5069+
)
5070+
5071+
# Should get a warning about the decorated fixture during collection with correct location
5072+
result.stdout.fnmatch_lines(
5073+
[
5074+
"*test_custom_decorated_fixture_warning.py:*: "
5075+
"PytestWarning: cannot discover my_fixture due to being wrapped in decorators*"
5076+
]
5077+
)
5078+
5079+
# The test should fail because fixture is not found
5080+
result.stdout.fnmatch_lines(["*fixture 'my_fixture' not found*"])
5081+
result.assert_outcomes(errors=1)

0 commit comments

Comments
 (0)