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/10819.deprecation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added a deprecation warning for class-scoped fixtures defined as instance methods (without ``@classmethod``). Such fixtures set attributes on a different instance than the test methods use, leading to unexpected behavior. Use ``@classmethod`` decorator instead -- by :user:`yastcher`.

See :issue:`10819` and :issue:`14011`.
47 changes: 47 additions & 0 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,53 @@ You can fix it by convert generators and iterators to lists or tuples:
Note that :class:`range` objects are ``Collection`` and are not affected by this deprecation.


.. _class-scoped-fixture-as-instance-method:

Class-scoped fixture as instance method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. deprecated:: 9.1

Defining a class-scoped fixture as an instance method (without ``@classmethod``) is deprecated
and will be removed in pytest 10.0.

When a class-scoped fixture is defined as an instance method, any attributes set on ``self``
will not be visible to test methods. This happens because pytest creates a new instance of the
test class for each test method, while the fixture runs only once per class on a different instance.

**Before** (deprecated):

.. code-block:: python

class TestExample:
@pytest.fixture(scope="class")
def setup_data(self):
self.data = [1, 2, 3] # This won't be visible to tests!

def test_something(self, setup_data):
assert self.data == [
1,
2,
3,
] # AttributeError: 'TestExample' object has no attribute 'data'

**After** (recommended):

.. code-block:: python

class TestExample:
@pytest.fixture(scope="class")
@classmethod
def setup_data(cls):
cls.data = [1, 2, 3]

def test_something(self, setup_data):
assert self.data == [1, 2, 3] # Works correctly

Using ``@classmethod`` ensures attributes are set on the class itself, making them accessible
to all test methods.


.. _monkeypatch-fixup-namespace-packages:

``monkeypatch.syspath_prepend`` with legacy namespace packages
Expand Down
8 changes: 8 additions & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
"Use @pytest.fixture instead; they are the same."
)

CLASS_FIXTURE_INSTANCE_METHOD = PytestRemovedIn10Warning(
"Class-scoped fixture defined as instance method is deprecated.\n"
"Instance attributes set in this fixture will NOT be visible to test methods,\n"
"as each test gets a new instance while the fixture runs only once per class.\n"
"Use @classmethod decorator and set attributes on cls instead.\n"
"See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to introduce a new section in deprecations.rst explaning this deprecation, including examples of how to update the code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. _class-scoped-fixture-as-instance-method:

Class-scoped fixture as instance method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. deprecated:: 9.1

Defining a class-scoped fixture as an instance method (without ``@classmethod``) is deprecated
and will be removed in pytest 10.0.

When a class-scoped fixture is defined as an instance method, any attributes set on ``self``
will not be visible to test methods. This happens because pytest creates a new instance of the
test class for each test method, while the fixture runs only once per class on a different instance.

**Before** (deprecated):

.. code-block:: python

    class TestExample:
        @pytest.fixture(scope="class")
        def setup_data(self):
            self.data = [1, 2, 3]  # This won't be visible to tests!

        def test_something(self, setup_data):
            assert self.data == [1, 2, 3]  # AttributeError: 'TestExample' object has no attribute 'data'

**After** (recommended):

.. code-block:: python

    class TestExample:
        @pytest.fixture(scope="class")
        @classmethod
        def setup_data(cls):
            cls.data = [1, 2, 3]

        def test_something(self, setup_data):
            assert self.data == [1, 2, 3]  # Works correctly

Using ``@classmethod`` ensures attributes are set on the class itself, making them accessible
to all test methods.

)

# This deprecation is never really meant to be removed.
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")

Expand Down
11 changes: 11 additions & 0 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from _pytest.config import ExitCode
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import CLASS_FIXTURE_INSTANCE_METHOD
from _pytest.deprecated import YIELD_FIXTURE
from _pytest.main import Session
from _pytest.mark import ParameterSet
Expand Down Expand Up @@ -1148,6 +1149,16 @@ def resolve_fixture_function(
# request.instance so that code working with "fixturedef" behaves
# as expected.
instance = request.instance

if fixturedef._scope is Scope.Class:
# Check if fixture is an instance method (bound to instance, not class)
if hasattr(fixturefunc, "__self__"):
bound_to = fixturefunc.__self__
# classmethod: bound_to is the class itself (a type)
# instance method: bound_to is an instance (not a type)
if not isinstance(bound_to, type):
warnings.warn(CLASS_FIXTURE_INSTANCE_METHOD, stacklevel=2)

if instance is not None:
# Handle the case where fixture is defined not in a test class, but some other class
# (for example a plugin class with a fixture), see #2270.
Expand Down
21 changes: 21 additions & 0 deletions testing/deprecated_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,24 @@ def collect(self):
parent=mod.parent,
fspath=legacy_path("bla"),
)


def test_class_scope_instance_method_is_deprecated(pytester: Pytester) -> None:
pytester.makepyfile(
"""
import pytest

class TestClass:
@pytest.fixture(scope="class")
def fix(self):
self.attr = True

def test_foo(self, fix):
pass
"""
)
result = pytester.runpytest("-Werror::pytest.PytestRemovedIn10Warning")
result.assert_outcomes(errors=1)
result.stdout.fnmatch_lines(
["*PytestRemovedIn10Warning: Class-scoped fixture defined as instance method*"]
)