diff --git a/changelog/9277.breaking.rst b/changelog/9277.breaking.rst new file mode 100644 index 00000000000..0296dcd7c99 --- /dev/null +++ b/changelog/9277.breaking.rst @@ -0,0 +1,3 @@ +The ``pytest.Instance`` collector type has been removed. +Importing ``pytest.Instance`` or ``_pytest.python.Instance`` returns a dummy type and emits a deprecation warning. +See :ref:`instance-collector-deprecation` for details. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 67859d2268e..c3d64d5880b 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -18,6 +18,25 @@ Deprecated Features Below is a complete list of all pytest features which are considered deprecated. Using those features will issue :class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +.. _instance-collector-deprecation: + +The ``pytest.Instance`` collector +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 7.0 + +The ``pytest.Instance`` collector type has been removed. + +Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`. +Now :class:`~pytest.Class` collects the test methods directly. + +Most plugins which reference ``Instance`` do so in order to ignore or skip it, +using a check such as ``if isinstance(node, Instance): return``. +Such plugins should simply remove consideration of ``Instance`` on pytest>=7. +However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``, +and importing it emits a deprecation warning. This will be removed in pytest 8. + + .. _node-ctor-fspath-deprecation: ``fspath`` argument for Node constructors replaced with ``pathlib.Path`` diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index ad1dfcb807c..d768d0770c5 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -119,6 +119,11 @@ "pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead", ) +INSTANCE_COLLECTOR = PytestDeprecationWarning( + "The pytest.Instance collector type is deprecated and is no longer used. " + "See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector", +) + # You want to make some `__init__` or function "private". # # def my_private_function(some, args): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ea59c2ca440..af5e7435048 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -58,6 +58,7 @@ from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH +from _pytest.deprecated import INSTANCE_COLLECTOR from _pytest.fixtures import FuncFixtureInfo from _pytest.main import Session from _pytest.mark import MARK_GEN @@ -868,14 +869,24 @@ def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]: self.obj.__pytest_setup_method = xunit_setup_method_fixture -# Instance used to be a node type between Class and Function. It has been -# removed in pytest 7.0. Some plugins exist which reference `pytest.Instance` -# only to ignore it; this dummy class keeps them working. This could probably -# be removed at some point. -class Instance: +class InstanceDummy: + """Instance used to be a node type between Class and Function. It has been + removed in pytest 7.0. Some plugins exist which reference `pytest.Instance` + only to ignore it; this dummy class keeps them working. This will be removed + in pytest 8.""" + pass +# Note: module __getattr__ only works on Python>=3.7. Unfortunately +# we can't provide this deprecation warning on Python 3.6. +def __getattr__(name: str) -> object: + if name == "Instance": + warnings.warn(INSTANCE_COLLECTOR, 2) + return InstanceDummy + raise AttributeError(f"module {__name__} has no attribute {name}") + + def hasinit(obj: object) -> bool: init: object = getattr(obj, "__init__", None) if init: diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 69aa90fdd9f..18e40ceae6a 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -48,7 +48,6 @@ from _pytest.pytester import RunResult from _pytest.python import Class from _pytest.python import Function -from _pytest.python import Instance from _pytest.python import Metafunc from _pytest.python import Module from _pytest.python import Package @@ -77,6 +76,7 @@ set_trace = __pytestPDB.set_trace + __all__ = [ "__version__", "_fillfuncargs", @@ -106,7 +106,6 @@ "HookRecorder", "hookspec", "importorskip", - "Instance", "Item", "LineMatcher", "LogCaptureFixture", @@ -153,3 +152,12 @@ "xfail", "yield_fixture", ] + + +def __getattr__(name: str) -> object: + if name == "Instance": + # The import emits a deprecation warning. + from _pytest.python import Instance + + return Instance + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5fa50e5cd0f..fb465e1e0dd 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -286,3 +286,21 @@ def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None: parent=mod.parent, fspath=legacy_path("bla"), ) + + +@pytest.mark.skipif( + sys.version_info < (3, 7), + reason="This deprecation can only be emitted on python>=3.7", +) +def test_importing_instance_is_deprecated(pytester: Pytester) -> None: + with pytest.warns( + pytest.PytestDeprecationWarning, + match=re.escape("The pytest.Instance collector type is deprecated"), + ): + pytest.Instance + + with pytest.warns( + pytest.PytestDeprecationWarning, + match=re.escape("The pytest.Instance collector type is deprecated"), + ): + from _pytest.python import Instance # noqa: F401