Skip to content

Internal AssertionError after class scoped fixture warning #14775

Description

@The-Compiler

On ced9022, running this with -Werror:

import pytest

class TestFixt:

    @pytest.fixture(scope="class")
    def fixt(self):
        yield

    def test_1(self, fixt):
        pass

    def test_2(self, fixt):
        pass

causes not just the intended error:

Details
================================= test session starts ==================================
platform linux -- Python 3.14.2, pytest-9.2.0.dev111+gced9022c0, pluggy-1.6.0
rootdir: /home/freya/proj/pytest
configfile: pyproject.toml
plugins: hypothesis-6.159.0
collected 2 items                                                                      

test_class_fixt.py EE                                                            [100%]

======================================== ERRORS ========================================
__________________________ ERROR at setup of TestFixt.test_1 ___________________________

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x7ff17a3545c0>, when = 'setup'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
    ) -> CallInfo[TResult]:
        """Call func, wrapping the result in a CallInfo.
    
        :param func:
            The function to call. Called without arguments.
        :type func: Callable[[], _pytest.runner.TResult]
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        instant = timing.Instant()
        try:
>           result: TResult | None = func()
                                     ^^^^^^

src/_pytest/runner.py:361: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/_pytest/runner.py:250: in <lambda>
    lambda: runtest_hook(item=item, **kwds),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/logging.py:858: in pytest_runtest_setup
    yield
src/_pytest/capture.py:895: in pytest_runtest_setup
    return (yield)
            ^^^^^
src/_pytest/runner.py:170: in pytest_runtest_setup
    item.session._setupstate.setup(item)
src/_pytest/runner.py:536: in setup
    col.setup()
src/_pytest/python.py:1784: in setup
    self._request._fillfixtures()
src/_pytest/fixtures.py:807: in _fillfixtures
    item.funcargs[argname] = self.getfixturevalue(argname)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/fixtures.py:631: in getfixturevalue
    fixturedef = self._get_active_fixturedef(argname)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/fixtures.py:727: in _get_active_fixturedef
    fixturedef.execute(request=subrequest)
src/_pytest/fixtures.py:1233: in execute
    result: FixtureValue = ihook.pytest_fixture_setup(
.venv/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/setuponly.py:36: in pytest_fixture_setup
    return (yield)
            ^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

fixturedef = <FixtureDef argname='fixt' scope='class' baseid='test_class_fixt.py::TestFixt'>
request = <SubRequest 'fixt' for <Function test_1>>

    def pytest_fixture_setup(
        fixturedef: FixtureDef[FixtureValue], request: SubRequest
    ) -> FixtureValue:
        """Execution of fixture setup."""
        kwargs = {}
        for argname in fixturedef.argnames:
            kwargs[argname] = request.getfixturevalue(argname)
    
>       fixturefunc = resolve_fixture_function(fixturedef, request)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       pytest.PytestRemovedIn10Warning: Class-scoped fixtures defined as instance methods are deprecated.
E       Instance attributes set in the 'fixt' fixture will NOT be visible to test methods,
E       as each test gets a new instance while the fixture runs only once per class.
E       Use a @classmethod decorator below @pytest.fixture and set attributes on cls instead.
E       See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method

but then results in an internal pytest error when setting up the second test:

__________________________ ERROR at setup of TestFixt.test_2 ___________________________

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x7ff17a9d1640>, when = 'setup'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
    ) -> CallInfo[TResult]:
        """Call func, wrapping the result in a CallInfo.
    
        :param func:
            The function to call. Called without arguments.
        :type func: Callable[[], _pytest.runner.TResult]
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        instant = timing.Instant()
        try:
>           result: TResult | None = func()
                                     ^^^^^^

src/_pytest/runner.py:361: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/_pytest/runner.py:250: in <lambda>
    lambda: runtest_hook(item=item, **kwds),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/logging.py:858: in pytest_runtest_setup
    yield
src/_pytest/capture.py:895: in pytest_runtest_setup
    return (yield)
            ^^^^^
src/_pytest/runner.py:170: in pytest_runtest_setup
    item.session._setupstate.setup(item)
src/_pytest/runner.py:536: in setup
    col.setup()
src/_pytest/python.py:1784: in setup
    self._request._fillfixtures()
src/_pytest/fixtures.py:807: in _fillfixtures
    item.funcargs[argname] = self.getfixturevalue(argname)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/fixtures.py:631: in getfixturevalue
    fixturedef = self._get_active_fixturedef(argname)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/_pytest/fixtures.py:727: in _get_active_fixturedef
    fixturedef.execute(request=subrequest)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <FixtureDef argname='fixt' scope='class' baseid='test_class_fixt.py::TestFixt'>
request = <SubRequest 'fixt' for <Function test_2>>

    def execute(self, request: SubRequest) -> FixtureValue:
        """Return the value of this fixture, executing it if not cached."""
        # Ensure that the dependent fixtures requested by this fixture are loaded.
        # This needs to be done before checking if we have a cached value, since
        # if a dependent fixture has their cache invalidated, e.g. due to
        # parametrization, they finalize themselves and fixtures depending on it
        # (which will likely include this fixture) setting `self.cached_result = None`.
        # See #4871
        requested_fixtures_that_should_finalize_us = []
        for argname in self.argnames:
            fixturedef = request._get_active_fixturedef(argname)
            # Saves requested fixtures in a list so we later can add our finalizer
            # to them, ensuring that if a requested fixture gets torn down we get torn
            # down first. This is generally handled by SetupState, but still currently
            # needed when this fixture is not parametrized but depends on a parametrized
            # fixture.
            requested_fixtures_that_should_finalize_us.append(fixturedef)
    
        # Check for (and return) cached value/exception.
        if self.cached_result is not None:
            request_cache_key = self.cache_key(request)
            cache_key = self.cached_result[1]
            try:
                # Attempt to make a normal == check: this might fail for objects
                # which do not implement the standard comparison (like numpy arrays -- #6497).
                cache_hit = bool(request_cache_key == cache_key)
            except (ValueError, RuntimeError):
                # If the comparison raises, use 'is' as fallback.
                cache_hit = request_cache_key is cache_key
    
            if cache_hit:
                if self.cached_result[2] is not None:
                    exc, exc_tb = self.cached_result[2]
                    raise exc.with_traceback(exc_tb)
                else:
                    return self.cached_result[0]
            # We have a previous but differently parametrized fixture instance
            # so we need to tear it down before creating a new one.
            self.finish(request)
            assert self.cached_result is None
    
        # Add finalizer to requested fixtures we saved previously.
        # We make sure to do this after checking for cached value to avoid
        # adding our finalizer multiple times. (#12135)
        finalizer = functools.partial(self.finish, request=request)
        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
               ^^^^^^^^^^^^^^^^^^^^
E       AssertionError

src/_pytest/fixtures.py:1222: AssertionError
=============================== short test summary info ================================
ERROR test_class_fixt.py::TestFixt::test_1 - pytest.PytestRemovedIn10Warning: Class-scoped fixtures defined as instance methods ...
ERROR test_class_fixt.py::TestFixt::test_2 - AssertionError
================================== 2 errors in 0.25s ===================================

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: fixturesanything involving fixtures directly or indirectlytype: bugproblem that needs to be addressed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions