From f43a8db618a7f0ef9c44a403993cab758eb16ef0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Sep 2023 10:38:58 +0300 Subject: [PATCH] Improve pluggy-related typing --- src/_pytest/config/__init__.py | 6 +++--- src/_pytest/config/compat.py | 39 ++++++++++++++++++++++------------ src/_pytest/main.py | 21 ++++++++++++------ src/_pytest/nodes.py | 13 ++++-------- 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index eb03b6338..447ebc42a 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -37,6 +37,7 @@ from typing import Type from typing import TYPE_CHECKING from typing import Union +import pluggy from pluggy import HookimplMarker from pluggy import HookimplOpts from pluggy import HookspecMarker @@ -46,6 +47,7 @@ from pluggy import PluginManager import _pytest._code import _pytest.deprecated import _pytest.hookspec +from .compat import PathAwareHookProxy from .exceptions import PrintHelp as PrintHelp from .exceptions import UsageError as UsageError from .findpaths import determine_setup @@ -1005,10 +1007,8 @@ class Config: # Deprecated alias. Was never public. Can be removed in a few releases. self._store = self.stash - from .compat import PathAwareHookProxy - self.trace = self.pluginmanager.trace.root.get("config") - self.hook = PathAwareHookProxy(self.pluginmanager.hook) + self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment] self._inicache: Dict[str, Any] = {} self._override_ini: Sequence[str] = () self._opt2dest: Dict[str, str] = {} diff --git a/src/_pytest/config/compat.py b/src/_pytest/config/compat.py index 5bd922a4a..afb38bbcc 100644 --- a/src/_pytest/config/compat.py +++ b/src/_pytest/config/compat.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import functools import warnings from pathlib import Path -from typing import Optional +from typing import Mapping + +import pluggy from ..compat import LEGACY_PATH from ..compat import legacy_path from ..deprecated import HOOK_LEGACY_PATH_ARG -from _pytest.nodes import _check_path # hookname: (Path, LEGACY_PATH) -imply_paths_hooks = { +imply_paths_hooks: Mapping[str, tuple[str, str]] = { "pytest_ignore_collect": ("collection_path", "path"), "pytest_collect_file": ("file_path", "path"), "pytest_pycollect_makemodule": ("module_path", "path"), @@ -18,6 +21,14 @@ imply_paths_hooks = { } +def _check_path(path: Path, fspath: LEGACY_PATH) -> None: + if Path(fspath) != path: + raise ValueError( + f"Path({fspath!r}) != {path!r}\n" + "if both path and fspath are given they need to be equal" + ) + + class PathAwareHookProxy: """ this helper wraps around hook callers @@ -27,24 +38,24 @@ class PathAwareHookProxy: this may have to be changed later depending on bugs """ - def __init__(self, hook_caller): - self.__hook_caller = hook_caller + def __init__(self, hook_relay: pluggy.HookRelay) -> None: + self._hook_relay = hook_relay - def __dir__(self): - return dir(self.__hook_caller) + def __dir__(self) -> list[str]: + return dir(self._hook_relay) - def __getattr__(self, key, _wraps=functools.wraps): - hook = getattr(self.__hook_caller, key) + def __getattr__(self, key: str) -> pluggy.HookCaller: + hook: pluggy.HookCaller = getattr(self._hook_relay, key) if key not in imply_paths_hooks: self.__dict__[key] = hook return hook else: path_var, fspath_var = imply_paths_hooks[key] - @_wraps(hook) + @functools.wraps(hook) def fixed_hook(**kw): - path_value: Optional[Path] = kw.pop(path_var, None) - fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None) + path_value: Path | None = kw.pop(path_var, None) + fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None) if fspath_value is not None: warnings.warn( HOOK_LEGACY_PATH_ARG.format( @@ -65,6 +76,8 @@ class PathAwareHookProxy: kw[fspath_var] = fspath_value return hook(**kw) + fixed_hook.name = hook.name # type: ignore[attr-defined] + fixed_hook.spec = hook.spec # type: ignore[attr-defined] fixed_hook.__name__ = key self.__dict__[key] = fixed_hook - return fixed_hook + return fixed_hook # type: ignore[return-value] diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d979f3f50..5cee8e89b 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -7,6 +7,7 @@ import importlib import os import sys from pathlib import Path +from typing import AbstractSet from typing import Callable from typing import Dict from typing import final @@ -22,6 +23,8 @@ from typing import Type from typing import TYPE_CHECKING from typing import Union +import pluggy + import _pytest._code from _pytest import nodes from _pytest.config import Config @@ -31,6 +34,7 @@ from _pytest.config import hookimpl from _pytest.config import PytestPluginManager from _pytest.config import UsageError from _pytest.config.argparsing import Parser +from _pytest.config.compat import PathAwareHookProxy from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.pathlib import absolutepath @@ -429,11 +433,15 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No class FSHookProxy: - def __init__(self, pm: PytestPluginManager, remove_mods) -> None: + def __init__( + self, + pm: PytestPluginManager, + remove_mods: AbstractSet[object], + ) -> None: self.pm = pm self.remove_mods = remove_mods - def __getattr__(self, name: str): + def __getattr__(self, name: str) -> pluggy.HookCaller: x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) self.__dict__[name] = x return x @@ -546,7 +554,7 @@ class Session(nodes.FSCollector): path_ = path if isinstance(path, Path) else Path(path) return path_ in self._initialpaths - def gethookproxy(self, fspath: "os.PathLike[str]"): + def gethookproxy(self, fspath: "os.PathLike[str]") -> pluggy.HookRelay: # Optimization: Path(Path(...)) is much slower than isinstance. path = fspath if isinstance(fspath, Path) else Path(fspath) pm = self.config.pluginmanager @@ -563,11 +571,10 @@ class Session(nodes.FSCollector): ) my_conftestmodules = pm._getconftestmodules(path) remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + proxy: pluggy.HookRelay if remove_mods: - # One or more conftests are not in use at this fspath. - from .config.compat import PathAwareHookProxy - - proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) + # One or more conftests are not in use at this path. + proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment] else: # All plugins are active for this fspath. proxy = self.config.hook diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index cb8907fe8..c06fa8ecd 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -19,6 +19,8 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +import pluggy + import _pytest._code from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo @@ -27,6 +29,7 @@ from _pytest._code.code import Traceback from _pytest.compat import LEGACY_PATH from _pytest.config import Config from _pytest.config import ConftestImportFailure +from _pytest.config.compat import _check_path from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import NODE_CTOR_FSPATH_ARG from _pytest.mark.structures import Mark @@ -94,14 +97,6 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]: yield nodeid -def _check_path(path: Path, fspath: LEGACY_PATH) -> None: - if Path(fspath) != path: - raise ValueError( - f"Path({fspath!r}) != {path!r}\n" - "if both path and fspath are given they need to be equal" - ) - - def _imply_path( node_type: Type["Node"], path: Optional[Path], @@ -264,7 +259,7 @@ class Node(metaclass=NodeMeta): return cls._create(parent=parent, **kw) @property - def ihook(self): + def ihook(self) -> pluggy.HookRelay: """fspath-sensitive hook proxy used to call pytest hooks.""" return self.session.gethookproxy(self.path)