From 010ce2ab0f6d2e90f295d4e3f9e725d31dd5721d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 23 Feb 2024 09:35:57 +0200 Subject: [PATCH] Add typing to `from_parent` return values (#11916) Up to now the return values of `from_parent` were untyped, this is an attempt to make it work with `typing.Self`. --- src/_pytest/doctest.py | 11 +++++------ src/_pytest/main.py | 11 ++++++++--- src/_pytest/nodes.py | 22 +++++++++++++--------- src/_pytest/python.py | 25 +++++++++++++------------ src/_pytest/unittest.py | 3 +-- testing/test_collection.py | 2 +- 6 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 8fe992b6a..ced3b82f5 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -47,6 +47,7 @@ from _pytest.warning_types import PytestWarning if TYPE_CHECKING: import doctest + from typing import Self DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" @@ -133,11 +134,9 @@ def pytest_collect_file( if config.option.doctestmodules and not any( (_is_setup_py(file_path), _is_main_py(file_path)) ): - mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path) - return mod + return DoctestModule.from_parent(parent, path=file_path) elif _is_doctest(config, file_path, parent): - txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path) - return txt + return DoctestTextfile.from_parent(parent, path=file_path) return None @@ -272,14 +271,14 @@ class DoctestItem(Item): self._initrequest() @classmethod - def from_parent( # type: ignore + def from_parent( # type: ignore[override] cls, parent: "Union[DoctestTextfile, DoctestModule]", *, name: str, runner: "doctest.DocTestRunner", dtest: "doctest.DocTest", - ): + ) -> "Self": # incompatible signature due to imposed limits on subclass """The public named constructor.""" return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 5c70ad74e..672ad67b9 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -21,6 +21,7 @@ from typing import Optional from typing import overload from typing import Sequence from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import warnings @@ -49,6 +50,10 @@ from _pytest.runner import SetupState from _pytest.warning_types import PytestWarning +if TYPE_CHECKING: + from typing import Self + + def pytest_addoption(parser: Parser) -> None: parser.addini( "norecursedirs", @@ -491,16 +496,16 @@ class Dir(nodes.Directory): @classmethod def from_parent( # type: ignore[override] cls, - parent: nodes.Collector, # type: ignore[override] + parent: nodes.Collector, *, path: Path, - ) -> "Dir": + ) -> "Self": """The public constructor. :param parent: The parent collector of this Dir. :param path: The directory's path. """ - return super().from_parent(parent=parent, path=path) # type: ignore[no-any-return] + return super().from_parent(parent=parent, path=path) def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: config = self.config diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 48f7d5841..2381b65ea 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -11,6 +11,7 @@ from typing import Iterable from typing import Iterator from typing import List from typing import MutableMapping +from typing import NoReturn from typing import Optional from typing import overload from typing import Set @@ -41,6 +42,8 @@ from _pytest.warning_types import PytestWarning if TYPE_CHECKING: + from typing import Self + # Imported here due to circular import. from _pytest._code.code import _TracebackStyle from _pytest.main import Session @@ -51,6 +54,7 @@ SEP = "/" tracebackcutdir = Path(_pytest.__file__).parent +_T = TypeVar("_T") _NodeType = TypeVar("_NodeType", bound="Node") @@ -69,33 +73,33 @@ class NodeMeta(abc.ABCMeta): progress on detangling the :class:`Node` classes. """ - def __call__(self, *k, **kw): + def __call__(cls, *k, **kw) -> NoReturn: msg = ( "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" "See " "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent" " for more details." - ).format(name=f"{self.__module__}.{self.__name__}") + ).format(name=f"{cls.__module__}.{cls.__name__}") fail(msg, pytrace=False) - def _create(self, *k, **kw): + def _create(cls: Type[_T], *k, **kw) -> _T: try: - return super().__call__(*k, **kw) + return super().__call__(*k, **kw) # type: ignore[no-any-return,misc] except TypeError: - sig = signature(getattr(self, "__init__")) + sig = signature(getattr(cls, "__init__")) known_kw = {k: v for k, v in kw.items() if k in sig.parameters} from .warning_types import PytestDeprecationWarning warnings.warn( PytestDeprecationWarning( - f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n" + f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n" "See https://docs.pytest.org/en/stable/deprecations.html" "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs " "for more details." ) ) - return super().__call__(*k, **known_kw) + return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc] class Node(abc.ABC, metaclass=NodeMeta): @@ -181,7 +185,7 @@ class Node(abc.ABC, metaclass=NodeMeta): self._store = self.stash @classmethod - def from_parent(cls, parent: "Node", **kw): + def from_parent(cls, parent: "Node", **kw) -> "Self": """Public constructor for Nodes. This indirection got introduced in order to enable removing @@ -583,7 +587,7 @@ class FSCollector(Collector, abc.ABC): *, path: Optional[Path] = None, **kw, - ): + ) -> "Self": """The public constructor.""" return super().from_parent(parent=parent, path=path, **kw) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index a060b17e5..ca64a877d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -27,6 +27,7 @@ from typing import Pattern from typing import Sequence from typing import Set from typing import Tuple +from typing import TYPE_CHECKING from typing import Union import warnings @@ -81,6 +82,10 @@ from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning +if TYPE_CHECKING: + from typing import Self + + _PYTEST_DIR = Path(_pytest.__file__).parent @@ -204,8 +209,7 @@ def pytest_collect_directory( ) -> Optional[nodes.Collector]: pkginit = path / "__init__.py" if pkginit.is_file(): - pkg: Package = Package.from_parent(parent, path=path) - return pkg + return Package.from_parent(parent, path=path) return None @@ -230,8 +234,7 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module": - mod: Module = Module.from_parent(parent, path=module_path) - return mod + return Module.from_parent(parent, path=module_path) @hookimpl(trylast=True) @@ -242,8 +245,7 @@ def pytest_pycollect_makeitem( # Nothing was collected elsewhere, let's do it here. if safe_isclass(obj): if collector.istestclass(obj, name): - klass: Class = Class.from_parent(collector, name=name, obj=obj) - return klass + return Class.from_parent(collector, name=name, obj=obj) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it. obj = getattr(obj, "__func__", obj) @@ -262,7 +264,7 @@ def pytest_pycollect_makeitem( ) elif getattr(obj, "__test__", True): if is_generator(obj): - res: Function = Function.from_parent(collector, name=name) + res = Function.from_parent(collector, name=name) reason = ( f"yield tests were removed in pytest 4.0 - {name} will be ignored" ) @@ -465,9 +467,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): clscol = self.getparent(Class) cls = clscol and clscol.obj or None - definition: FunctionDefinition = FunctionDefinition.from_parent( - self, name=name, callobj=funcobj - ) + definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) fixtureinfo = definition._fixtureinfo # pytest_generate_tests impls call metafunc.parametrize() which fills @@ -751,7 +751,7 @@ class Class(PyCollector): """Collector for test methods (and nested classes) in a Python class.""" @classmethod - def from_parent(cls, parent, *, name, obj=None, **kw): + def from_parent(cls, parent, *, name, obj=None, **kw) -> "Self": # type: ignore[override] """The public constructor.""" return super().from_parent(name=name, parent=parent, **kw) @@ -1730,8 +1730,9 @@ class Function(PyobjMixin, nodes.Item): self.fixturenames = fixtureinfo.names_closure self._initrequest() + # todo: determine sound type limitations @classmethod - def from_parent(cls, parent, **kw): # todo: determine sound type limitations + def from_parent(cls, parent, **kw) -> "Self": """The public constructor.""" return super().from_parent(parent=parent, **kw) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 6598bdbc5..2b7966531 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -55,8 +55,7 @@ def pytest_pycollect_makeitem( except Exception: return None # Yes, so let's collect it. - item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj) - return item + return UnitTestCase.from_parent(collector, name=name, obj=obj) class UnitTestCase(Class): diff --git a/testing/test_collection.py b/testing/test_collection.py index 98cff8fe9..de8cacb8f 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1613,7 +1613,7 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) -> assert collector.x == 10 -def test_class_from_parent(pytester: Pytester, request: FixtureRequest) -> None: +def test_class_from_parent(request: FixtureRequest) -> None: """Ensure Class.from_parent can forward custom arguments to the constructor.""" class MyCollector(pytest.Class):