Skip to content

Commit 3835460

Browse files
Overriding fixtures at the same level now triggers a warning.
1 parent 28e1e25 commit 3835460

File tree

3 files changed

+94
-0
lines changed

3 files changed

+94
-0
lines changed

changelog/12952.improvement.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Overriding fixtures at the same level is considered unintended behavior, now triggers a warning.

src/_pytest/fixtures.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,6 +1791,10 @@ def parsefactories(
17911791
holderobj_tp = holderobj
17921792

17931793
self._holderobjseen.add(holderobj)
1794+
1795+
# Collect different implementations of the same fixture to check for duplicates.
1796+
fixture_name_map: dict[str, list[str]] = {}
1797+
17941798
for name in dir(holderobj):
17951799
# The attribute can be an arbitrary descriptor, so the attribute
17961800
# access below can raise. safe_getattr() ignores such exceptions.
@@ -1811,6 +1815,9 @@ def parsefactories(
18111815

18121816
func = obj._get_wrapped_function()
18131817

1818+
fixture_name_map.setdefault(fixture_name, [])
1819+
fixture_name_map[fixture_name].append(f"{func!r}")
1820+
18141821
self._register_fixture(
18151822
name=fixture_name,
18161823
nodeid=nodeid,
@@ -1821,6 +1828,35 @@ def parsefactories(
18211828
autouse=marker.autouse,
18221829
)
18231830

1831+
# Check different implementations of the same fixture (#12952).
1832+
not_by_plugin = nodeid or getattr(holderobj, "__name__", "") == "conftest"
1833+
1834+
# If the fixture from a plugin, Skip check.
1835+
if not_by_plugin:
1836+
for fixture_name, func_list in fixture_name_map.items():
1837+
if len(func_list) > 1:
1838+
msg = (
1839+
f"Fixture definition conflict: \n"
1840+
f"{fixture_name!r} has multiple implementations:"
1841+
f"{func_list!r}"
1842+
)
1843+
1844+
if isinstance(node_or_obj, nodes.Node):
1845+
node_or_obj.warn(PytestWarning(msg))
1846+
else:
1847+
filename = getattr(node_or_obj, "__file__", None)
1848+
lineno = 1
1849+
if filename is None:
1850+
filename = inspect.getfile(type(node_or_obj))
1851+
lineno = inspect.getsourcelines(type(node_or_obj))[1]
1852+
1853+
warnings.warn_explicit(
1854+
PytestWarning(msg),
1855+
category=None,
1856+
filename=filename,
1857+
lineno=lineno,
1858+
)
1859+
18241860
def getfixturedefs(
18251861
self, argname: str, node: nodes.Node
18261862
) -> Sequence[FixtureDef[Any]] | None:

testing/python/fixtures.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5009,3 +5009,60 @@ def test_result():
50095009
)
50105010
result = pytester.runpytest()
50115011
assert result.ret == 0
5012+
5013+
5014+
@pytest.mark.filterwarnings("default")
5015+
def test_fixture_name_conflict(pytester: Pytester) -> None:
5016+
"""Repetitive coverage at the same level is an unexpected behavior (#12952)."""
5017+
pytester.makepyfile(
5018+
"""
5019+
import pytest
5020+
5021+
@pytest.fixture(name="cache")
5022+
def c1(): # Create first, but register later
5023+
return 1
5024+
5025+
@pytest.fixture(name="cache")
5026+
def c0(): # Create later, but register first
5027+
return 0
5028+
5029+
def test_value(cache):
5030+
assert cache == 0 # Failed, `cache` from c1
5031+
"""
5032+
)
5033+
5034+
result = pytester.runpytest()
5035+
result.stdout.fnmatch_lines(["* PytestWarning: Fixture definition conflict:*"])
5036+
5037+
5038+
@pytest.mark.filterwarnings("default")
5039+
def test_fixture_name_conflict_with_conftest(pytester: Pytester) -> None:
5040+
"""
5041+
Related to #12952,
5042+
pyester is unable to capture warnings and errors from root conftest.
5043+
So in this tests will cover it.
5044+
"""
5045+
pytester.makeini("[pytest]")
5046+
pytester.makeconftest(
5047+
"""
5048+
import pytest
5049+
5050+
@pytest.fixture(name="cache")
5051+
def c1(): # Create first, but register later
5052+
return 1
5053+
5054+
@pytest.fixture(name="cache")
5055+
def c0(): # Create later, but register first
5056+
return 0
5057+
"""
5058+
)
5059+
5060+
pytester.makepyfile(
5061+
"""
5062+
def test_value(cache):
5063+
assert cache == 0 # Failed, `cache` from c1
5064+
"""
5065+
)
5066+
5067+
with pytest.warns(pytest.PytestWarning):
5068+
pytester.runpytest()

0 commit comments

Comments
 (0)