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
6 changes: 6 additions & 0 deletions changelog/14774.deprecation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Similarly to the deprecation of class-scoped fixtures defined as instance methods in pytest 9.1,
*all* higher-scoped fixtures (``module``, ``package``, and ``session``) defined
as instance methods are now deprecated.

They suffer from the same issue the original deprecation only fixed for
class-scoped fixtures, see :issue:`10819` and :issue:`14011`.
5 changes: 5 additions & 0 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@ test class for each test method, while the fixture runs only once per class on a
Using ``@classmethod`` ensures attributes are set on the class itself, making them accessible
to all test methods.

.. deprecated:: 9.2

Similarly, using a scope higher than ``class`` (``module``, ``package`` or ``session``)
without using ``@classmethod`` (or ``@staticmethod``) is deprecated and will be
removed in pytest 10.0.

.. _monkeypatch-fixup-namespace-packages:

Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

CLASS_FIXTURE_INSTANCE_METHOD = UnformattedWarning(
PytestRemovedIn10Warning,
"Class-scoped fixtures defined as instance methods are deprecated.\n"
"{scope}-scoped fixtures defined as instance methods are deprecated.\n"
"Instance attributes set in the {fixturename!r} 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 a @classmethod decorator below @pytest.fixture and set attributes on cls instead.\n"
Expand Down
5 changes: 3 additions & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,7 @@ def __tracebackhide__(e):
# as expected.
instance = request.instance

if fixturedef._scope is Scope.Class:
if fixturedef._scope >= Scope.Class:
# Check if fixture is an instance method (bound to instance, not class)
if hasattr(fixturefunc, "__self__"):
bound_to = fixturefunc.__self__
Expand All @@ -1293,7 +1293,8 @@ def __tracebackhide__(e):
if not isinstance(bound_to, type):
warnings.warn(
CLASS_FIXTURE_INSTANCE_METHOD.format(
fixturename=request.fixturename
scope=fixturedef._scope.name.capitalize(),
fixturename=request.fixturename,
),
stacklevel=2,
)
Expand Down
50 changes: 40 additions & 10 deletions testing/deprecated_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from _pytest import deprecated
from _pytest.pytester import Pytester
from _pytest.scope import Scope
import pytest
from pytest import PytestDeprecationWarning

Expand Down Expand Up @@ -87,13 +88,18 @@ def __init__(self, foo: int, *, _ispytest: bool = False) -> None:
PrivateInit(10, _ispytest=True)


def test_class_scope_instance_method_is_deprecated(pytester: Pytester) -> None:
@pytest.mark.parametrize(
"scope", [Scope.Class, Scope.Module, Scope.Package, Scope.Session]
)
def test_higher_scope_instance_method_is_deprecated(
pytester: Pytester, scope: Scope
) -> None:
pytester.makepyfile(
"""
f"""
import pytest

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

Expand All @@ -104,20 +110,23 @@ def test_foo(self, fix):
result = pytester.runpytest("-Werror::pytest.PytestRemovedIn10Warning")
result.assert_outcomes(errors=1)
result.stdout.fnmatch_lines(
[
"*PytestRemovedIn10Warning: Class-scoped fixtures defined as instance methods*"
]
["*PytestRemovedIn10Warning: *-scoped fixtures defined as instance methods*"]
)


def test_class_scope_classmethod_fixture_not_deprecated(pytester: Pytester) -> None:
"""A class-scoped fixture defined as @classmethod does NOT warn."""
@pytest.mark.parametrize(
"scope", [Scope.Class, Scope.Module, Scope.Package, Scope.Session]
)
def test_higher_scope_classmethod_fixture_not_deprecated(
pytester: Pytester, scope: Scope
) -> None:
"""A higher-scoped fixture defined as @classmethod does NOT warn."""
pytester.makepyfile(
"""
f"""
import pytest

class TestClass:
@pytest.fixture(scope="class")
@pytest.fixture(scope="{scope.value}")
@classmethod
def fix(cls):
cls.attr = True
Expand All @@ -130,6 +139,27 @@ def test_foo(self, fix):
result.assert_outcomes(passed=1)


@pytest.mark.parametrize("scope", list(Scope))
def test_staticmethod_fixture_not_deprecated(pytester: Pytester, scope: Scope) -> None:
"""A fixture at any scope defined as @staticmethod does NOT warn."""
pytester.makepyfile(
f"""
import pytest

class TestClass:
@pytest.fixture(scope="{scope.value}")
@staticmethod
def fix():
pass

def test_foo(self, fix):
pass
"""
)
result = pytester.runpytest("-Werror::pytest.PytestRemovedIn10Warning")
result.assert_outcomes(passed=1)


class TestFixtureNodeidDeprecations:
"""Tests for deprecated baseid/nodeid string APIs in fixture registration.

Expand Down
3 changes: 2 additions & 1 deletion testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1292,7 +1292,8 @@ def test_1(arg):

class TestRequestSessionScoped:
@pytest.fixture(scope="session")
def session_request(self, request):
@staticmethod
def session_request(request):
return request

@pytest.mark.parametrize("name", ["path", "module"])
Expand Down
3 changes: 2 additions & 1 deletion testing/test_conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def conftest_setinitial(
@pytest.mark.usefixtures("_sys_snapshot")
class TestConftestValueAccessGlobal:
@pytest.fixture(scope="module", params=["global", "inpackage"])
def basedir(self, request, tmp_path_factory: TempPathFactory) -> Generator[Path]:
@staticmethod
def basedir(request, tmp_path_factory: TempPathFactory) -> Generator[Path]:
tmp_path = tmp_path_factory.mktemp("basedir", numbered=True)
tmp_path.joinpath("adir/b").mkdir(parents=True)
tmp_path.joinpath("adir/conftest.py").write_text(
Expand Down
3 changes: 2 additions & 1 deletion testing/test_legacypath.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None:

class TestFixtureRequestSessionScoped:
@pytest.fixture(scope="session")
def session_request(self, request):
@staticmethod
def session_request(request):
return request

def test_session_scoped_unavailable_attributes(self, session_request):
Expand Down
8 changes: 5 additions & 3 deletions testing/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,10 @@ class TestImportPath:
"""

@pytest.fixture(scope="session")
def path1(self, tmp_path_factory: TempPathFactory) -> Generator[Path]:
@classmethod
def path1(cls, tmp_path_factory: TempPathFactory) -> Generator[Path]:
path = tmp_path_factory.mktemp("path")
self.setuptestfs(path)
cls.setuptestfs(path)
yield path
assert path.joinpath("samplefile").exists()

Expand All @@ -141,7 +142,8 @@ def preserve_sys(self):
with unittest.mock.patch.object(sys, "path", list(sys.path)):
yield

def setuptestfs(self, path: Path) -> None:
@staticmethod
def setuptestfs(path: Path) -> None:
# print "setting up test fs for", repr(path)
samplefile = path / "samplefile"
samplefile.write_text("samplefile\n", encoding="utf-8")
Expand Down