Improve pluggy-related typing

This commit is contained in:
Ran Benita 2023-09-15 10:38:58 +03:00
parent 39f9306357
commit f43a8db618
4 changed files with 47 additions and 32 deletions

View File

@ -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] = {}

View File

@ -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]

View File

@ -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

View File

@ -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)