Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ pip-wheel-metadata/

# pytest debug logs generated via --debug
pytestdebug.log
.claude/settings.local.json
2 changes: 2 additions & 0 deletions changelog/13564.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Issue a warning when fixtures are wrapped with a decorator, as that excludes
them from being discovered safely by pytest.
36 changes: 35 additions & 1 deletion src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1271,7 +1271,9 @@ def __init__(
def __repr__(self) -> str:
return f"<pytest_fixture({self._fixture_function})>"

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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading