Skip to content
Open
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 changelog/3225.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A non-autouse fixture overriding an autouse one no longer runs automatically, without changing explicit fixture requests.
23 changes: 18 additions & 5 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ def __init__(
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
*,
node: nodes.Node | NotSetType = NOTSET,
# only used in a deprecationwarning msg, can be removed in pytest9
# Whether the fixture is autouse; consulted during fixture closure (#3225).
_autouse: bool = False,
_ispytest: bool = False,
) -> None:
Expand Down Expand Up @@ -1186,7 +1186,7 @@ def __init__(
self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
self._finalizers: Final[list[Callable[[], object]]] = []

# only used to emit a deprecationwarning, can be removed in pytest9
# Whether the fixture is autouse; consulted during fixture closure (#3225).
self._autouse = _autouse

@property
Expand Down Expand Up @@ -1938,14 +1938,27 @@ def pytest_collection_finish(self) -> None:

def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
"""Return the names of autouse fixtures visible to node."""
usefixtures_ini = set(self.config.getini("usefixtures"))
for parentnode in node.listchain():
basenames = self._node_autousenames.get(parentnode)
if basenames:
yield from basenames
# Legacy fallback: check string-based nodeid autouse names.
for name in basenames:
if name in usefixtures_ini or self._is_autouse(name, node):
yield name
# Legacy fallback: string-based nodeid autouse names.
nodeid_basenames = self._nodeid_autousenames.get(parentnode.nodeid)
if nodeid_basenames:
yield from nodeid_basenames
for name in nodeid_basenames:
if self._is_autouse(name, node):
yield name

def _is_autouse(self, name: str, node: nodes.Node) -> bool:
"""Whether the fixture resolved for name is itself autouse.

A non-autouse override cancels the autouse fixture it shadows (#3225).
"""
fixturedefs = self.getfixturedefs(name, node)
return not fixturedefs or fixturedefs[-1]._autouse

def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
"""Return the names of usefixtures fixtures visible to node."""
Expand Down
106 changes: 106 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2560,6 +2560,112 @@ def test_hello(arg1):
reprec = pytester.inline_run()
reprec.assertoutcome(passed=1)

def test_autouse_cancelled_by_non_autouse_override(
self, pytester: Pytester
) -> None:
"""A non-autouse override cancels the autouse fixture it shadows (#3225)."""
pytester.makeconftest(
"""
import pytest

@pytest.fixture(autouse=True)
def foo():
pass
"""
)
pytester.makepyfile(
"""
import pytest

@pytest.fixture()
def foo():
assert False

def test_bar(foo):
pass

def test_baz():
pass
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=1, errors=1)
result = pytester.runpytest("-o", "usefixtures=foo")
result.assert_outcomes(errors=2)

def test_autouse_cancelled_by_non_autouse_class_override(
self, pytester: Pytester
) -> None:
"""A non-autouse class-level override cancels the autouse fixture (#3225)."""
pytester.makepyfile(
"""
import pytest

@pytest.fixture(autouse=True)
def foo():
pass

class TestClass:
@pytest.fixture()
def foo(self):
assert False

def test_with_request(self, foo):
pass

def test_no_request(self):
pass

def test_module_level():
pass
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=2, errors=1)

def test_getautousenames_legacy_nodeid_autouse(self, pytester: Pytester) -> None:
"""Autouse registered via the deprecated nodeid API is still yielded."""
pytester.makeconftest(
"""
import pytest

@pytest.fixture
def fm(request):
return request._fixturemanager

@pytest.fixture
def item(request):
return request._pyfuncitem
"""
)
pytester.makepyfile(
"""
import warnings

def test_legacy(item, fm):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fm._register_fixture(
name="legacy_auto",
func=lambda: None,
nodeid=item.nodeid,
autouse=True,
)
assert "legacy_auto" in list(fm._getautousenames(item))
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fm._register_fixture(
name="legacy_auto",
func=lambda: None,
nodeid=item.nodeid,
autouse=False,
)
assert "legacy_auto" not in list(fm._getautousenames(item))
"""
)
reprec = pytester.inline_run()
reprec.assertoutcome(passed=1)

@pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"])
@pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"])
def test_ordering_dependencies_torndown_first(
Expand Down