Skip to content

Commit 424b1ac

Browse files
committed
deprecate: warn on class-scoped fixture as instance method (#10819) (#14011)
1 parent e95a843 commit 424b1ac

File tree

5 files changed

+135
-0
lines changed

5 files changed

+135
-0
lines changed

changelog/10819.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed class-scoped fixtures defined in base classes not binding to the correct test instance when inherited by child test classes -- by :user:`yastcher`.
2+
3+
Fixes :issue:`10819` and :issue:`14011`.

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+
)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from __future__ import annotations
2+
3+
import typing
4+
5+
import pytest
6+
7+
8+
class ParentBase:
9+
"""from issue #14011"""
10+
11+
name = ""
12+
variable = ""
13+
flag: bool
14+
15+
@classmethod
16+
def setup(cls) -> None:
17+
cls.variable = cls.name
18+
19+
@classmethod
20+
def teardown(cls) -> None:
21+
pass
22+
23+
@pytest.fixture(scope="class")
24+
@classmethod
25+
def fix(cls) -> typing.Generator[None]:
26+
cls.setup()
27+
yield
28+
cls.teardown()
29+
30+
@pytest.fixture(scope="class", autouse=True)
31+
@classmethod
32+
def base_autouse(cls) -> None:
33+
cls.flag = True
34+
35+
36+
@pytest.mark.usefixtures("fix")
37+
class Test1(ParentBase):
38+
name = "test1"
39+
40+
def test_a(self) -> None:
41+
assert self.variable == self.name
42+
43+
44+
@pytest.mark.usefixtures("fix")
45+
class Test2(ParentBase):
46+
name = "test2"
47+
48+
def test_a(self) -> None:
49+
assert self.variable == self.name
50+
51+
52+
class TestChild(ParentBase):
53+
def test_flag(self) -> None:
54+
assert self.flag
55+
56+
57+
class BaseTestClass:
58+
"""from issue #10819"""
59+
60+
test_func_scope_set = None
61+
test_class_scope_set = None
62+
63+
@pytest.fixture(scope="class", autouse=True)
64+
@classmethod
65+
def dummy_class_fixture(cls) -> None:
66+
cls.test_class_scope_set = True
67+
68+
@pytest.fixture(scope="function", autouse=True)
69+
def dummy_func_fixture(self) -> None:
70+
self.test_func_scope_set = True
71+
72+
73+
class TestDummy(BaseTestClass):
74+
def test_dummy(self) -> None:
75+
assert self.test_func_scope_set is True
76+
assert self.test_class_scope_set is True
77+
78+
79+
@pytest.mark.usefixtures("fix")
80+
class TestMultipleMethods(ParentBase):
81+
"""check class-scoped fixture with multiple test methods"""
82+
83+
name = "multi"
84+
85+
def test_a(self) -> None:
86+
assert self.variable == self.name
87+
88+
def test_b(self) -> None:
89+
assert self.variable == self.name
90+
91+
def test_c(self) -> None:
92+
assert self.variable == self.name

0 commit comments

Comments
 (0)