Skip to content

Commit 397769c

Browse files
authored
Merge pull request #11677 from bluetech/nodes-abc
nodes,python: mark abstract node classes as ABCs
2 parents d167564 + 0ae02e2 commit 397769c

File tree

9 files changed

+39
-12
lines changed

9 files changed

+39
-12
lines changed

changelog/11676.breaking.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The classes :class:`~_pytest.nodes.Node`, :class:`~pytest.Collector`, :class:`~pytest.Item`, :class:`~pytest.File`, :class:`~_pytest.nodes.FSCollector` are now marked abstract (see :mod:`abc`).
2+
3+
We do not expect this change to affect users and plugin authors, it will only cause errors when the code is already wrong or problematic.

doc/en/reference/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@ Node
821821

822822
.. autoclass:: _pytest.nodes.Node()
823823
:members:
824+
:show-inheritance:
824825

825826
Collector
826827
~~~~~~~~~

src/_pytest/fixtures.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ def get_scope_node(
135135
import _pytest.python
136136

137137
if scope is Scope.Function:
138-
return node.getparent(nodes.Item)
138+
# Type ignored because this is actually safe, see:
139+
# https://github.com/python/mypy/issues/4717
140+
return node.getparent(nodes.Item) # type: ignore[type-abstract]
139141
elif scope is Scope.Class:
140142
return node.getparent(_pytest.python.Class)
141143
elif scope is Scope.Module:

src/_pytest/nodes.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import abc
12
import os
23
import warnings
34
from functools import cached_property
@@ -121,7 +122,7 @@ def _imply_path(
121122
_NodeType = TypeVar("_NodeType", bound="Node")
122123

123124

124-
class NodeMeta(type):
125+
class NodeMeta(abc.ABCMeta):
125126
"""Metaclass used by :class:`Node` to enforce that direct construction raises
126127
:class:`Failed`.
127128
@@ -165,7 +166,7 @@ def _create(self, *k, **kw):
165166
return super().__call__(*k, **known_kw)
166167

167168

168-
class Node(metaclass=NodeMeta):
169+
class Node(abc.ABC, metaclass=NodeMeta):
169170
r"""Base class of :class:`Collector` and :class:`Item`, the components of
170171
the test collection tree.
171172
@@ -534,7 +535,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
534535
return getattr(node, "fspath", "unknown location"), -1
535536

536537

537-
class Collector(Node):
538+
class Collector(Node, abc.ABC):
538539
"""Base class of all collectors.
539540
540541
Collector create children through `collect()` and thus iteratively build
@@ -544,6 +545,7 @@ class Collector(Node):
544545
class CollectError(Exception):
545546
"""An error during collection, contains a custom message."""
546547

548+
@abc.abstractmethod
547549
def collect(self) -> Iterable[Union["Item", "Collector"]]:
548550
"""Collect children (items and collectors) for this collector."""
549551
raise NotImplementedError("abstract")
@@ -588,7 +590,7 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
588590
return None
589591

590592

591-
class FSCollector(Collector):
593+
class FSCollector(Collector, abc.ABC):
592594
"""Base class for filesystem collectors."""
593595

594596
def __init__(
@@ -666,14 +668,14 @@ def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
666668
return self.session.isinitpath(path)
667669

668670

669-
class File(FSCollector):
671+
class File(FSCollector, abc.ABC):
670672
"""Base class for collecting tests from a file.
671673
672674
:ref:`non-python tests`.
673675
"""
674676

675677

676-
class Item(Node):
678+
class Item(Node, abc.ABC):
677679
"""Base class of all test invocation items.
678680
679681
Note that for a single function there might be multiple test invocation items.
@@ -739,6 +741,7 @@ def _check_item_and_collector_diamond_inheritance(self) -> None:
739741
PytestWarning,
740742
)
741743

744+
@abc.abstractmethod
742745
def runtest(self) -> None:
743746
"""Run the test case for this item.
744747

src/_pytest/python.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Python test discovery, setup and run of test functions."""
2+
import abc
23
import dataclasses
34
import enum
45
import fnmatch
@@ -380,7 +381,7 @@ class _EmptyClass: pass # noqa: E701
380381
# fmt: on
381382

382383

383-
class PyCollector(PyobjMixin, nodes.Collector):
384+
class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
384385
def funcnamefilter(self, name: str) -> bool:
385386
return self._matches_prefix_or_glob_option("python_functions", name)
386387

testing/deprecated_test.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,17 @@ def pytest_cmdline_preparse(config, args):
257257
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
258258
mod = pytester.getmodulecol("")
259259

260+
class MyFile(pytest.File):
261+
def collect(self):
262+
raise NotImplementedError()
263+
260264
with pytest.warns(
261265
pytest.PytestDeprecationWarning,
262-
match=re.escape("The (fspath: py.path.local) argument to File is deprecated."),
266+
match=re.escape(
267+
"The (fspath: py.path.local) argument to MyFile is deprecated."
268+
),
263269
):
264-
pytest.File.from_parent(
270+
MyFile.from_parent(
265271
parent=mod.parent,
266272
fspath=legacy_path("bla"),
267273
)

testing/example_scripts/issue88_initial_file_multinodes/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ def pytest_collect_file(file_path, parent):
1111

1212

1313
class MyItem(pytest.Item):
14-
pass
14+
def runtest(self):
15+
raise NotImplementedError()

testing/test_collection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ def test_getcustomfile_roundtrip(self, pytester: Pytester) -> None:
9999
conftest="""
100100
import pytest
101101
class CustomFile(pytest.File):
102-
pass
102+
def collect(self):
103+
return []
103104
def pytest_collect_file(file_path, parent):
104105
if file_path.suffix == ".xxx":
105106
return CustomFile.from_parent(path=file_path, parent=parent)
@@ -1509,6 +1510,9 @@ def __init__(self, *k, x, **kw):
15091510
super().__init__(*k, **kw)
15101511
self.x = x
15111512

1513+
def collect(self):
1514+
raise NotImplementedError()
1515+
15121516
collector = MyCollector.from_parent(
15131517
parent=request.session, path=pytester.path / "foo", x=10
15141518
)

testing/test_nodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ def __init__(self, fspath, parent):
7373
"""Legacy ctor with legacy call # don't wana see"""
7474
super().__init__(fspath, parent)
7575

76+
def collect(self):
77+
raise NotImplementedError()
78+
79+
def runtest(self):
80+
raise NotImplementedError()
81+
7682
with pytest.warns(PytestWarning) as rec:
7783
SoWrong.from_parent(
7884
request.session, fspath=legacy_path(tmp_path / "broken.txt")

0 commit comments

Comments
 (0)