Skip to content

Commit 1100ad2

Browse files
committed
deprecate: warn on class-scoped fixture as instance method (#10819) (#14011)
1 parent 5f59a74 commit 1100ad2

5 files changed

Lines changed: 86 additions & 0 deletions

File tree

changelog/10819.deprecation.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
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`.
2+
3+
See :issue:`10819` and :issue:`14011`.

doc/en/deprecations.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,49 @@ You can fix it by convert generators and iterators to lists or tuples:
113113
Note that :class:`range` objects are ``Collection`` and are not affected by this deprecation.
114114

115115

116+
.. _class-scoped-fixture-as-instance-method:
117+
118+
Class-scoped fixture as instance method
119+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
120+
121+
.. deprecated:: 9.1
122+
123+
Defining a class-scoped fixture as an instance method (without ``@classmethod``) is deprecated
124+
and will be removed in pytest 10.0.
125+
126+
When a class-scoped fixture is defined as an instance method, any attributes set on ``self``
127+
will not be visible to test methods. This happens because pytest creates a new instance of the
128+
test class for each test method, while the fixture runs only once per class on a different instance.
129+
130+
**Before** (deprecated):
131+
132+
.. code-block:: python
133+
134+
class TestExample:
135+
@pytest.fixture(scope="class")
136+
def setup_data(self):
137+
self.data = [1, 2, 3] # This won't be visible to tests!
138+
139+
def test_something(self, setup_data):
140+
assert self.data == [1, 2, 3] # AttributeError: 'TestExample' object has no attribute 'data'
141+
142+
**After** (recommended):
143+
144+
.. code-block:: python
145+
146+
class TestExample:
147+
@pytest.fixture(scope="class")
148+
@classmethod
149+
def setup_data(cls):
150+
cls.data = [1, 2, 3]
151+
152+
def test_something(self, setup_data):
153+
assert self.data == [1, 2, 3] # Works correctly
154+
155+
Using ``@classmethod`` ensures attributes are set on the class itself, making them accessible
156+
to all test methods.
157+
158+
116159
.. _monkeypatch-fixup-namespace-packages:
117160

118161
``monkeypatch.syspath_prepend`` with legacy namespace packages

src/_pytest/deprecated.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
"Use @pytest.fixture instead; they are the same."
3636
)
3737

38+
CLASS_FIXTURE_INSTANCE_METHOD = PytestRemovedIn10Warning(
39+
"Class-scoped fixture defined as instance method is deprecated.\n"
40+
"Instance attributes set in this fixture will NOT be visible to test methods,\n"
41+
"as each test gets a new instance while the fixture runs only once per class.\n"
42+
"Use @classmethod decorator and set attributes on cls instead.\n"
43+
"See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method"
44+
)
45+
3846
# This deprecation is never really meant to be removed.
3947
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
4048

src/_pytest/fixtures.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from _pytest.config import ExitCode
5555
from _pytest.config.argparsing import Parser
5656
from _pytest.deprecated import check_ispytest
57+
from _pytest.deprecated import CLASS_FIXTURE_INSTANCE_METHOD
5758
from _pytest.deprecated import YIELD_FIXTURE
5859
from _pytest.main import Session
5960
from _pytest.mark import ParameterSet
@@ -1148,6 +1149,16 @@ def resolve_fixture_function(
11481149
# request.instance so that code working with "fixturedef" behaves
11491150
# as expected.
11501151
instance = request.instance
1152+
1153+
if fixturedef._scope is Scope.Class:
1154+
# Check if fixture is an instance method (bound to instance, not class)
1155+
if hasattr(fixturefunc, "__self__"):
1156+
bound_to = fixturefunc.__self__
1157+
# classmethod: bound_to is the class itself (a type)
1158+
# instance method: bound_to is an instance (not a type)
1159+
if not isinstance(bound_to, type):
1160+
warnings.warn(CLASS_FIXTURE_INSTANCE_METHOD, stacklevel=2)
1161+
11511162
if instance is not None:
11521163
# Handle the case where fixture is defined not in a test class, but some other class
11531164
# (for example a plugin class with a fixture), see #2270.

testing/deprecated_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,24 @@ def collect(self):
107107
parent=mod.parent,
108108
fspath=legacy_path("bla"),
109109
)
110+
111+
112+
def test_class_scope_instance_method_is_deprecated(pytester: Pytester) -> None:
113+
pytester.makepyfile(
114+
"""
115+
import pytest
116+
117+
class TestClass:
118+
@pytest.fixture(scope="class")
119+
def fix(self):
120+
self.attr = True
121+
122+
def test_foo(self, fix):
123+
pass
124+
"""
125+
)
126+
result = pytester.runpytest("-Werror::pytest.PytestRemovedIn10Warning")
127+
result.assert_outcomes(errors=1)
128+
result.stdout.fnmatch_lines(
129+
["*PytestRemovedIn10Warning: Class-scoped fixture defined as instance method*"]
130+
)

0 commit comments

Comments
 (0)