Skip to content

Commit 062d91a

Browse files
authored
python: remove the Instance collector node
1 parent 17b3825 commit 062d91a

19 files changed

+142
-65
lines changed

changelog/9277.breaking.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The ``pytest.Instance`` collector type has been removed.
2+
Importing ``pytest.Instance`` or ``_pytest.python.Instance`` returns a dummy type and emits a deprecation warning.
3+
See :ref:`instance-collector-deprecation` for details.

doc/en/deprecations.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@ Deprecated Features
1818
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
1919
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
2020

21+
.. _instance-collector-deprecation:
22+
23+
The ``pytest.Instance`` collector
24+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25+
26+
.. versionremoved:: 7.0
27+
28+
The ``pytest.Instance`` collector type has been removed.
29+
30+
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
31+
Now :class:`~pytest.Class` collects the test methods directly.
32+
33+
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
34+
using a check such as ``if isinstance(node, Instance): return``.
35+
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
36+
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
37+
and importing it emits a deprecation warning. This will be removed in pytest 8.
38+
39+
2140
.. _node-ctor-fspath-deprecation:
2241

2342
``fspath`` argument for Node constructors replaced with ``pathlib.Path``

src/_pytest/deprecated.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@
119119
"pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead",
120120
)
121121

122+
INSTANCE_COLLECTOR = PytestDeprecationWarning(
123+
"The pytest.Instance collector type is deprecated and is no longer used. "
124+
"See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector",
125+
)
126+
122127
# You want to make some `__init__` or function "private".
123128
#
124129
# def my_private_function(some, args):

src/_pytest/junitxml.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,10 +450,6 @@ def pytest_unconfigure(config: Config) -> None:
450450
def mangle_test_address(address: str) -> List[str]:
451451
path, possible_open_bracket, params = address.partition("[")
452452
names = path.split("::")
453-
try:
454-
names.remove("()")
455-
except ValueError:
456-
pass
457453
# Convert file path to dotted path.
458454
names[0] = names[0].replace(nodes.SEP, ".")
459455
names[0] = re.sub(r"\.py$", "", names[0])

src/_pytest/main.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -781,9 +781,6 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
781781
submatchnodes.append(r)
782782
if submatchnodes:
783783
work.append((submatchnodes, matchnames[1:]))
784-
# XXX Accept IDs that don't have "()" for class instances.
785-
elif len(rep.result) == 1 and rep.result[0].name == "()":
786-
work.append((rep.result, matchnames))
787784
else:
788785
# Report collection failures here to avoid failing to run some test
789786
# specified in the command line because the module could not be

src/_pytest/mark/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def from_item(cls, item: "Item") -> "KeywordMatcher":
158158
import pytest
159159

160160
for node in item.listchain():
161-
if not isinstance(node, (pytest.Instance, pytest.Session)):
161+
if not isinstance(node, pytest.Session):
162162
mapped_names.add(node.name)
163163

164164
# Add the names added as extra keywords to current or parent items.

src/_pytest/nodes.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,7 @@ def __init__(
225225
else:
226226
if not self.parent:
227227
raise TypeError("nodeid or parent must be provided")
228-
self._nodeid = self.parent.nodeid
229-
if self.name != "()":
230-
self._nodeid += "::" + self.name
228+
self._nodeid = self.parent.nodeid + "::" + self.name
231229

232230
#: A place where plugins can store information on the node for their
233231
#: own use.

src/_pytest/python.py

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from _pytest.config.argparsing import Parser
5959
from _pytest.deprecated import check_ispytest
6060
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
61+
from _pytest.deprecated import INSTANCE_COLLECTOR
6162
from _pytest.fixtures import FuncFixtureInfo
6263
from _pytest.main import Session
6364
from _pytest.mark import MARK_GEN
@@ -275,20 +276,14 @@ def cls(self):
275276
node = self.getparent(Class)
276277
return node.obj if node is not None else None
277278

278-
@property
279-
def instance(self):
280-
"""Python instance object this node was collected from (can be None)."""
281-
node = self.getparent(Instance)
282-
return node.obj if node is not None else None
283-
284279
@property
285280
def obj(self):
286281
"""Underlying Python object."""
287282
obj = getattr(self, "_obj", None)
288283
if obj is None:
289284
self._obj = obj = self._getobj()
290285
# XXX evil hack
291-
# used to avoid Instance collector marker duplication
286+
# used to avoid Function marker duplication
292287
if self._ALLOW_MARKERS:
293288
self.own_markers.extend(get_unpacked_marks(self.obj))
294289
return obj
@@ -310,8 +305,6 @@ def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) ->
310305
chain.reverse()
311306
parts = []
312307
for node in chain:
313-
if isinstance(node, Instance):
314-
continue
315308
name = node.name
316309
if isinstance(node, Module):
317310
name = os.path.splitext(name)[0]
@@ -410,8 +403,9 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
410403

411404
# Avoid random getattrs and peek in the __dict__ instead.
412405
dicts = [getattr(self.obj, "__dict__", {})]
413-
for basecls in self.obj.__class__.__mro__:
414-
dicts.append(basecls.__dict__)
406+
if isinstance(self.obj, type):
407+
for basecls in self.obj.__mro__:
408+
dicts.append(basecls.__dict__)
415409

416410
# In each class, nodes should be definition ordered. Since Python 3.6,
417411
# __dict__ is definition ordered.
@@ -491,7 +485,6 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
491485
self,
492486
name=subname,
493487
callspec=callspec,
494-
callobj=funcobj,
495488
fixtureinfo=fixtureinfo,
496489
keywords={callspec.id: True},
497490
originalname=name,
@@ -776,6 +769,9 @@ def from_parent(cls, parent, *, name, obj=None, **kw):
776769
"""The public constructor."""
777770
return super().from_parent(name=name, parent=parent, **kw)
778771

772+
def newinstance(self):
773+
return self.obj()
774+
779775
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
780776
if not safe_getattr(self.obj, "__test__", True):
781777
return []
@@ -803,7 +799,9 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
803799
self._inject_setup_class_fixture()
804800
self._inject_setup_method_fixture()
805801

806-
return [Instance.from_parent(self, name="()")]
802+
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
803+
804+
return super().collect()
807805

808806
def _inject_setup_class_fixture(self) -> None:
809807
"""Inject a hidden autouse, class scoped fixture into the collected class object
@@ -874,25 +872,22 @@ def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
874872
self.obj.__pytest_setup_method = xunit_setup_method_fixture
875873

876874

877-
class Instance(PyCollector):
878-
_ALLOW_MARKERS = False # hack, destroy later
879-
# Instances share the object with their parents in a way
880-
# that duplicates markers instances if not taken out
881-
# can be removed at node structure reorganization time.
875+
class InstanceDummy:
876+
"""Instance used to be a node type between Class and Function. It has been
877+
removed in pytest 7.0. Some plugins exist which reference `pytest.Instance`
878+
only to ignore it; this dummy class keeps them working. This will be removed
879+
in pytest 8."""
882880

883-
def _getobj(self):
884-
# TODO: Improve the type of `parent` such that assert/ignore aren't needed.
885-
assert self.parent is not None
886-
obj = self.parent.obj # type: ignore[attr-defined]
887-
return obj()
881+
pass
888882

889-
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
890-
self.session._fixturemanager.parsefactories(self)
891-
return super().collect()
892883

893-
def newinstance(self):
894-
self.obj = self._getobj()
895-
return self.obj
884+
# Note: module __getattr__ only works on Python>=3.7. Unfortunately
885+
# we can't provide this deprecation warning on Python 3.6.
886+
def __getattr__(name: str) -> object:
887+
if name == "Instance":
888+
warnings.warn(INSTANCE_COLLECTOR, 2)
889+
return InstanceDummy
890+
raise AttributeError(f"module {__name__} has no attribute {name}")
896891

897892

898893
def hasinit(obj: object) -> bool:
@@ -1686,9 +1681,23 @@ def function(self):
16861681
"""Underlying python 'function' object."""
16871682
return getimfunc(self.obj)
16881683

1684+
@property
1685+
def instance(self):
1686+
"""Python instance object the function is bound to.
1687+
1688+
Returns None if not a test method, e.g. for a standalone test function
1689+
or a staticmethod.
1690+
"""
1691+
return getattr(self.obj, "__self__", None)
1692+
16891693
def _getobj(self):
16901694
assert self.parent is not None
1691-
return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
1695+
if isinstance(self.parent, Class):
1696+
# Each Function gets a fresh class instance.
1697+
parent_obj = self.parent.newinstance()
1698+
else:
1699+
parent_obj = self.parent.obj # type: ignore[attr-defined]
1700+
return getattr(parent_obj, self.originalname)
16921701

16931702
@property
16941703
def _pyfuncitem(self):
@@ -1700,9 +1709,6 @@ def runtest(self) -> None:
17001709
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
17011710

17021711
def setup(self) -> None:
1703-
if isinstance(self.parent, Instance):
1704-
self.parent.newinstance()
1705-
self.obj = self._getobj()
17061712
self._request._fillfixtures()
17071713

17081714
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:

src/_pytest/terminal.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -756,9 +756,6 @@ def pytest_collection_finish(self, session: "Session") -> None:
756756
rep.toterminal(self._tw)
757757

758758
def _printcollecteditems(self, items: Sequence[Item]) -> None:
759-
# To print out items and their parent collectors
760-
# we take care to leave out Instances aka ()
761-
# because later versions are going to get rid of them anyway.
762759
if self.config.option.verbose < 0:
763760
if self.config.option.verbose < -1:
764761
counts = Counter(item.nodeid.split("::", 1)[0] for item in items)
@@ -778,8 +775,6 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None:
778775
stack.pop()
779776
for col in needed_collectors[len(stack) :]:
780777
stack.append(col)
781-
if col.name == "()": # Skip Instances.
782-
continue
783778
indent = (len(stack) - 1) * " "
784779
self._tw.line(f"{indent}{col}")
785780
if self.config.option.verbose >= 1:

src/pytest/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
from _pytest.pytester import RunResult
4949
from _pytest.python import Class
5050
from _pytest.python import Function
51-
from _pytest.python import Instance
5251
from _pytest.python import Metafunc
5352
from _pytest.python import Module
5453
from _pytest.python import Package
@@ -77,6 +76,7 @@
7776

7877
set_trace = __pytestPDB.set_trace
7978

79+
8080
__all__ = [
8181
"__version__",
8282
"_fillfuncargs",
@@ -106,7 +106,6 @@
106106
"HookRecorder",
107107
"hookspec",
108108
"importorskip",
109-
"Instance",
110109
"Item",
111110
"LineMatcher",
112111
"LogCaptureFixture",
@@ -153,3 +152,12 @@
153152
"xfail",
154153
"yield_fixture",
155154
]
155+
156+
157+
def __getattr__(name: str) -> object:
158+
if name == "Instance":
159+
# The import emits a deprecation warning.
160+
from _pytest.python import Instance
161+
162+
return Instance
163+
raise AttributeError(f"module {__name__} has no attribute {name}")

src/pytest/collect.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"Collector",
1212
"Module",
1313
"Function",
14-
"Instance",
1514
"Session",
1615
"Item",
1716
"Class",

testing/deprecated_test.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,21 @@ def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
286286
parent=mod.parent,
287287
fspath=legacy_path("bla"),
288288
)
289+
290+
291+
@pytest.mark.skipif(
292+
sys.version_info < (3, 7),
293+
reason="This deprecation can only be emitted on python>=3.7",
294+
)
295+
def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
296+
with pytest.warns(
297+
pytest.PytestDeprecationWarning,
298+
match=re.escape("The pytest.Instance collector type is deprecated"),
299+
):
300+
pytest.Instance
301+
302+
with pytest.warns(
303+
pytest.PytestDeprecationWarning,
304+
match=re.escape("The pytest.Instance collector type is deprecated"),
305+
):
306+
from _pytest.python import Instance # noqa: F401

testing/python/collect.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from _pytest.nodes import Collector
1313
from _pytest.pytester import Pytester
1414
from _pytest.python import Class
15-
from _pytest.python import Instance
15+
from _pytest.python import Function
1616

1717

1818
class TestModule:
@@ -585,7 +585,7 @@ def test2(self, x, y):
585585
pass
586586
"""
587587
)
588-
colitems = modcol.collect()[0].collect()[0].collect()
588+
colitems = modcol.collect()[0].collect()
589589
assert colitems[0].name == "test1[a-c]"
590590
assert colitems[1].name == "test1[b-c]"
591591
assert colitems[2].name == "test2[a-c]"
@@ -1183,19 +1183,26 @@ def test_reportinfo_with_nasty_getattr(self, pytester: Pytester) -> None:
11831183
modcol = pytester.getmodulecol(
11841184
"""
11851185
# lineno 0
1186-
class TestClass(object):
1186+
class TestClass:
11871187
def __getattr__(self, name):
11881188
return "this is not an int"
11891189
1190+
def __class_getattr__(cls, name):
1191+
return "this is not an int"
1192+
11901193
def intest_foo(self):
11911194
pass
1195+
1196+
def test_bar(self):
1197+
pass
11921198
"""
11931199
)
11941200
classcol = pytester.collect_by_name(modcol, "TestClass")
11951201
assert isinstance(classcol, Class)
1196-
instance = list(classcol.collect())[0]
1197-
assert isinstance(instance, Instance)
1198-
path, lineno, msg = instance.reportinfo()
1202+
path, lineno, msg = classcol.reportinfo()
1203+
func = list(classcol.collect())[0]
1204+
assert isinstance(func, Function)
1205+
path, lineno, msg = func.reportinfo()
11991206

12001207

12011208
def test_customized_python_discovery(pytester: Pytester) -> None:

0 commit comments

Comments
 (0)