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
3 changes: 3 additions & 0 deletions changelog/11281.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixed autouse fixtures with the same name at different scopes (e.g., module and class)
so that all of them execute. Previously, only the closest-scoped fixture would run,
silently skipping broader-scoped autouse fixtures with the same name.
64 changes: 63 additions & 1 deletion src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,19 @@ def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]:
raise FixtureLookupError(argname, self)
fixturedef = fixturedefs[index]

# When an autouse fixture shadows a broader-scoped autouse fixture
# with the same name (e.g., class-level "setup" shadows module-level
# "setup"), both should run -- the broader-scoped one first.
# If the closer fixture doesn't explicitly request its super (i.e.,
# argname not in its own argnames), the broader-scoped autouse
# fixture would never be activated. Ensure it runs here. (#11281)
if (
fixturedef._autouse
and argname not in fixturedef.argnames
and len(fixturedefs) > 1
):
self._ensure_autouse_super_fixtures(argname, fixturedefs, index)

# Prepare a SubRequest object for calling the fixture.
try:
callspec = self._pyfuncitem.callspec
Expand Down Expand Up @@ -622,6 +635,42 @@ def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]:
self._fixture_defs[argname] = fixturedef
return fixturedef

def _ensure_autouse_super_fixtures(
self,
argname: str,
fixturedefs: Sequence[FixtureDef[Any]],
index: int,
) -> None:
"""Ensure broader-scoped autouse fixtures in the override chain are executed.

When an autouse fixture at a closer scope (e.g., class) shadows an
autouse fixture at a broader scope (e.g., module) with the same name,
the broader-scoped fixture would not run because the closer one does
not explicitly request it. This method activates the broader-scoped
autouse fixtures so they run first, preserving the documented behavior
that higher-scoped fixtures execute first.

See issue #11281.
"""
# fixturedefs is ordered from broadest to closest scope.
# index is negative (-1 = closest). We want to activate all
# broader-scoped autouse fixtures that come before the active one.
active_pos = len(fixturedefs) + index
for i in range(active_pos):
super_fixturedef = fixturedefs[i]
if not super_fixturedef._autouse:
continue
if super_fixturedef.cached_result is not None:
# Already executed (e.g., by a module-level test).
continue
# Execute the broader-scoped autouse fixture.
super_scope = super_fixturedef._scope
self._check_scope(super_fixturedef, super_scope)
super_subrequest = SubRequest(
self, super_scope, NOTSET, 0, super_fixturedef, _ispytest=True
)
super_fixturedef.execute(request=super_subrequest)

def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None:
"""Check that this request is allowed to execute this fixturedef without
a param."""
Expand Down Expand Up @@ -1028,6 +1077,11 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
self._finalizers.append(finalizer)

def finish(self, request: SubRequest) -> None:
if self.cached_result is None:
# Already finished. It is assumed that finalizers cannot be added in
# this state.
return

exceptions: list[BaseException] = []
while self._finalizers:
fin = self._finalizers.pop()
Expand All @@ -1036,7 +1090,6 @@ def finish(self, request: SubRequest) -> None:
except BaseException as e:
exceptions.append(e)
node = request.node
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
# Even if finalization fails, we invalidate the cached fixture
# value and remove all finalizers because they may be bound methods
# which will keep instances alive.
Expand Down Expand Up @@ -1096,6 +1149,15 @@ def execute(self, request: SubRequest) -> FixtureValue:
for parent_fixture in requested_fixtures_that_should_finalize_us:
parent_fixture.addfinalizer(finalizer)

# Register the pytest_fixture_post_finalizer as the first finalizer,
# which is executed last.
assert not self._finalizers
self.addfinalizer(
lambda: request.node.ihook.pytest_fixture_post_finalizer(
fixturedef=self, request=request
)
)

ihook = request.node.ihook
try:
# Setup the fixture, run the code in it, and cache the value
Expand Down
69 changes: 69 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -5426,3 +5426,72 @@ def test_foobar(self, fixture_bar):
)
result = pytester.runpytest("-v")
result.assert_outcomes(passed=1)


def test_autouse_same_name_module_and_class_both_run(pytester: Pytester) -> None:
"""When module-scope and class-scope autouse fixtures share the same name,
both should execute for tests inside the class, with the module-scoped
fixture running first (higher scope executes first).

Regression test for https://github.com/pytest-dev/pytest/issues/11281
"""
pytester.makepyfile(
"""
import pytest

call_order = []

@pytest.fixture(scope="module", autouse=True)
def setup():
call_order.append("MODULE")

class TestFoo:
@pytest.fixture(scope="class", autouse=True)
def setup(self):
call_order.append("CLASS")

def test_in_class(self):
assert call_order == ["MODULE", "CLASS"]

def test_module():
# Module-level test only sees the module fixture.
assert "MODULE" in call_order
"""
)
result = pytester.runpytest("-v")
result.assert_outcomes(passed=2)


def test_autouse_same_name_conftest_and_module_both_run(pytester: Pytester) -> None:
"""When a conftest autouse fixture and a module autouse fixture share the
same name, both should execute, with the conftest (broader scope) running
first.

Regression test for https://github.com/pytest-dev/pytest/issues/11281
"""
pytester.makeconftest(
"""
import pytest

call_order = []

@pytest.fixture(scope="session", autouse=True)
def setup():
call_order.append("SESSION")
"""
)
pytester.makepyfile(
"""
import pytest
from conftest import call_order

@pytest.fixture(scope="module", autouse=True)
def setup():
call_order.append("MODULE")

def test_both_run():
assert call_order == ["SESSION", "MODULE"]
"""
)
result = pytester.runpytest("-v")
result.assert_outcomes(passed=1)
Loading