Merge pull request #11677 from bluetech/nodes-abc
nodes,python: mark abstract node classes as ABCs
This commit is contained in:
commit
397769c45e
|
@ -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.
|
|
@ -821,6 +821,7 @@ Node
|
||||||
|
|
||||||
.. autoclass:: _pytest.nodes.Node()
|
.. autoclass:: _pytest.nodes.Node()
|
||||||
:members:
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Collector
|
Collector
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue