Merge pull request #11677 from bluetech/nodes-abc

nodes,python: mark abstract node classes as ABCs
This commit is contained in:
Ran Benita 2023-12-10 09:41:46 +02:00 committed by GitHub
commit 397769c45e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 39 additions and 12 deletions

View File

@ -0,0 +1,3 @@
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`).
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.

View File

@ -821,6 +821,7 @@ Node
.. autoclass:: _pytest.nodes.Node() .. autoclass:: _pytest.nodes.Node()
:members: :members:
:show-inheritance:
Collector Collector
~~~~~~~~~ ~~~~~~~~~

View File

@ -135,7 +135,9 @@ def get_scope_node(
import _pytest.python import _pytest.python
if scope is Scope.Function: if scope is Scope.Function:
return node.getparent(nodes.Item) # Type ignored because this is actually safe, see:
# https://github.com/python/mypy/issues/4717
return node.getparent(nodes.Item) # type: ignore[type-abstract]
elif scope is Scope.Class: elif scope is Scope.Class:
return node.getparent(_pytest.python.Class) return node.getparent(_pytest.python.Class)
elif scope is Scope.Module: elif scope is Scope.Module:

View File

@ -1,3 +1,4 @@
import abc
import os import os
import warnings import warnings
from functools import cached_property from functools import cached_property
@ -121,7 +122,7 @@ def _imply_path(
_NodeType = TypeVar("_NodeType", bound="Node") _NodeType = TypeVar("_NodeType", bound="Node")
class NodeMeta(type): class NodeMeta(abc.ABCMeta):
"""Metaclass used by :class:`Node` to enforce that direct construction raises """Metaclass used by :class:`Node` to enforce that direct construction raises
:class:`Failed`. :class:`Failed`.
@ -165,7 +166,7 @@ class NodeMeta(type):
return super().__call__(*k, **known_kw) return super().__call__(*k, **known_kw)
class Node(metaclass=NodeMeta): class Node(abc.ABC, metaclass=NodeMeta):
r"""Base class of :class:`Collector` and :class:`Item`, the components of r"""Base class of :class:`Collector` and :class:`Item`, the components of
the test collection tree. the test collection tree.
@ -534,7 +535,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
return getattr(node, "fspath", "unknown location"), -1 return getattr(node, "fspath", "unknown location"), -1
class Collector(Node): class Collector(Node, abc.ABC):
"""Base class of all collectors. """Base class of all collectors.
Collector create children through `collect()` and thus iteratively build Collector create children through `collect()` and thus iteratively build
@ -544,6 +545,7 @@ class Collector(Node):
class CollectError(Exception): class CollectError(Exception):
"""An error during collection, contains a custom message.""" """An error during collection, contains a custom message."""
@abc.abstractmethod
def collect(self) -> Iterable[Union["Item", "Collector"]]: def collect(self) -> Iterable[Union["Item", "Collector"]]:
"""Collect children (items and collectors) for this collector.""" """Collect children (items and collectors) for this collector."""
raise NotImplementedError("abstract") raise NotImplementedError("abstract")
@ -588,7 +590,7 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
return None return None
class FSCollector(Collector): class FSCollector(Collector, abc.ABC):
"""Base class for filesystem collectors.""" """Base class for filesystem collectors."""
def __init__( def __init__(
@ -666,14 +668,14 @@ class FSCollector(Collector):
return self.session.isinitpath(path) return self.session.isinitpath(path)
class File(FSCollector): class File(FSCollector, abc.ABC):
"""Base class for collecting tests from a file. """Base class for collecting tests from a file.
:ref:`non-python tests`. :ref:`non-python tests`.
""" """
class Item(Node): class Item(Node, abc.ABC):
"""Base class of all test invocation items. """Base class of all test invocation items.
Note that for a single function there might be multiple test invocation items. Note that for a single function there might be multiple test invocation items.
@ -739,6 +741,7 @@ class Item(Node):
PytestWarning, PytestWarning,
) )
@abc.abstractmethod
def runtest(self) -> None: def runtest(self) -> None:
"""Run the test case for this item. """Run the test case for this item.

View File

@ -1,4 +1,5 @@
"""Python test discovery, setup and run of test functions.""" """Python test discovery, setup and run of test functions."""
import abc
import dataclasses import dataclasses
import enum import enum
import fnmatch import fnmatch
@ -380,7 +381,7 @@ del _EmptyClass
# fmt: on # fmt: on
class PyCollector(PyobjMixin, nodes.Collector): class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
def funcnamefilter(self, name: str) -> bool: def funcnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_functions", name) return self._matches_prefix_or_glob_option("python_functions", name)

View File

@ -257,11 +257,17 @@ def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None: def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
mod = pytester.getmodulecol("") mod = pytester.getmodulecol("")
class MyFile(pytest.File):
def collect(self):
raise NotImplementedError()
with pytest.warns( with pytest.warns(
pytest.PytestDeprecationWarning, pytest.PytestDeprecationWarning,
match=re.escape("The (fspath: py.path.local) argument to File is deprecated."), match=re.escape(
"The (fspath: py.path.local) argument to MyFile is deprecated."
),
): ):
pytest.File.from_parent( MyFile.from_parent(
parent=mod.parent, parent=mod.parent,
fspath=legacy_path("bla"), fspath=legacy_path("bla"),
) )

View File

@ -11,4 +11,5 @@ def pytest_collect_file(file_path, parent):
class MyItem(pytest.Item): class MyItem(pytest.Item):
pass def runtest(self):
raise NotImplementedError()

View File

@ -99,7 +99,8 @@ class TestCollector:
conftest=""" conftest="""
import pytest import pytest
class CustomFile(pytest.File): class CustomFile(pytest.File):
pass def collect(self):
return []
def pytest_collect_file(file_path, parent): def pytest_collect_file(file_path, parent):
if file_path.suffix == ".xxx": if file_path.suffix == ".xxx":
return CustomFile.from_parent(path=file_path, parent=parent) return CustomFile.from_parent(path=file_path, parent=parent)
@ -1509,6 +1510,9 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) ->
super().__init__(*k, **kw) super().__init__(*k, **kw)
self.x = x self.x = x
def collect(self):
raise NotImplementedError()
collector = MyCollector.from_parent( collector = MyCollector.from_parent(
parent=request.session, path=pytester.path / "foo", x=10 parent=request.session, path=pytester.path / "foo", x=10
) )

View File

@ -73,6 +73,12 @@ def test_subclassing_both_item_and_collector_deprecated(
"""Legacy ctor with legacy call # don't wana see""" """Legacy ctor with legacy call # don't wana see"""
super().__init__(fspath, parent) super().__init__(fspath, parent)
def collect(self):
raise NotImplementedError()
def runtest(self):
raise NotImplementedError()
with pytest.warns(PytestWarning) as rec: with pytest.warns(PytestWarning) as rec:
SoWrong.from_parent( SoWrong.from_parent(
request.session, fspath=legacy_path(tmp_path / "broken.txt") request.session, fspath=legacy_path(tmp_path / "broken.txt")