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 TYPE_CHECKING
from typing import Union from typing import Union
import pluggy
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookimplOpts from pluggy import HookimplOpts
from pluggy import HookspecMarker from pluggy import HookspecMarker
@ -46,6 +47,7 @@ from pluggy import PluginManager
import _pytest._code import _pytest._code
import _pytest.deprecated import _pytest.deprecated
import _pytest.hookspec import _pytest.hookspec
from .compat import PathAwareHookProxy
from .exceptions import PrintHelp as PrintHelp from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError from .exceptions import UsageError as UsageError
from .findpaths import determine_setup from .findpaths import determine_setup
@ -1005,10 +1007,8 @@ class Config:
# Deprecated alias. Was never public. Can be removed in a few releases. # Deprecated alias. Was never public. Can be removed in a few releases.
self._store = self.stash self._store = self.stash
from .compat import PathAwareHookProxy
self.trace = self.pluginmanager.trace.root.get("config") 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._inicache: Dict[str, Any] = {}
self._override_ini: Sequence[str] = () self._override_ini: Sequence[str] = ()
self._opt2dest: Dict[str, str] = {} self._opt2dest: Dict[str, str] = {}

View File

@ -1,15 +1,18 @@
from __future__ import annotations
import functools import functools
import warnings import warnings
from pathlib import Path 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 ..compat import legacy_path from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG from ..deprecated import HOOK_LEGACY_PATH_ARG
from _pytest.nodes import _check_path
# hookname: (Path, LEGACY_PATH) # hookname: (Path, LEGACY_PATH)
imply_paths_hooks = { imply_paths_hooks: Mapping[str, tuple[str, str]] = {
"pytest_ignore_collect": ("collection_path", "path"), "pytest_ignore_collect": ("collection_path", "path"),
"pytest_collect_file": ("file_path", "path"), "pytest_collect_file": ("file_path", "path"),
"pytest_pycollect_makemodule": ("module_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: class PathAwareHookProxy:
""" """
this helper wraps around hook callers this helper wraps around hook callers
@ -27,24 +38,24 @@ class PathAwareHookProxy:
this may have to be changed later depending on bugs this may have to be changed later depending on bugs
""" """
def __init__(self, hook_caller): def __init__(self, hook_relay: pluggy.HookRelay) -> None:
self.__hook_caller = hook_caller self._hook_relay = hook_relay
def __dir__(self): def __dir__(self) -> list[str]:
return dir(self.__hook_caller) return dir(self._hook_relay)
def __getattr__(self, key, _wraps=functools.wraps): def __getattr__(self, key: str) -> pluggy.HookCaller:
hook = getattr(self.__hook_caller, key) hook: pluggy.HookCaller = getattr(self._hook_relay, key)
if key not in imply_paths_hooks: if key not in imply_paths_hooks:
self.__dict__[key] = hook self.__dict__[key] = hook
return hook return hook
else: else:
path_var, fspath_var = imply_paths_hooks[key] path_var, fspath_var = imply_paths_hooks[key]
@_wraps(hook) @functools.wraps(hook)
def fixed_hook(**kw): def fixed_hook(**kw):
path_value: Optional[Path] = kw.pop(path_var, None) path_value: Path | None = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None) fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
if fspath_value is not None: if fspath_value is not None:
warnings.warn( warnings.warn(
HOOK_LEGACY_PATH_ARG.format( HOOK_LEGACY_PATH_ARG.format(
@ -65,6 +76,8 @@ class PathAwareHookProxy:
kw[fspath_var] = fspath_value kw[fspath_var] = fspath_value
return hook(**kw) 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 fixed_hook.__name__ = key
self.__dict__[key] = fixed_hook 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 os
import sys import sys
from pathlib import Path from pathlib import Path
from typing import AbstractSet
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import final from typing import final
@ -22,6 +23,8 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import pluggy
import _pytest._code import _pytest._code
from _pytest import nodes from _pytest import nodes
from _pytest.config import Config from _pytest.config import Config
@ -31,6 +34,7 @@ from _pytest.config import hookimpl
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
from _pytest.config import UsageError from _pytest.config import UsageError
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.config.compat import PathAwareHookProxy
from _pytest.fixtures import FixtureManager from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@ -429,11 +433,15 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No
class FSHookProxy: class FSHookProxy:
def __init__(self, pm: PytestPluginManager, remove_mods) -> None: def __init__(
self,
pm: PytestPluginManager,
remove_mods: AbstractSet[object],
) -> None:
self.pm = pm self.pm = pm
self.remove_mods = remove_mods 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) x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
self.__dict__[name] = x self.__dict__[name] = x
return x return x
@ -546,7 +554,7 @@ class Session(nodes.FSCollector):
path_ = path if isinstance(path, Path) else Path(path) path_ = path if isinstance(path, Path) else Path(path)
return path_ in self._initialpaths 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. # Optimization: Path(Path(...)) is much slower than isinstance.
path = fspath if isinstance(fspath, Path) else Path(fspath) path = fspath if isinstance(fspath, Path) else Path(fspath)
pm = self.config.pluginmanager pm = self.config.pluginmanager
@ -563,11 +571,10 @@ class Session(nodes.FSCollector):
) )
my_conftestmodules = pm._getconftestmodules(path) my_conftestmodules = pm._getconftestmodules(path)
remove_mods = pm._conftest_plugins.difference(my_conftestmodules) remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
proxy: pluggy.HookRelay
if remove_mods: if remove_mods:
# One or more conftests are not in use at this fspath. # One or more conftests are not in use at this path.
from .config.compat import PathAwareHookProxy proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment]
proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods))
else: else:
# All plugins are active for this fspath. # All plugins are active for this fspath.
proxy = self.config.hook proxy = self.config.hook

View File

@ -19,6 +19,8 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import pluggy
import _pytest._code import _pytest._code
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
@ -27,6 +29,7 @@ from _pytest._code.code import Traceback
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
from _pytest.config.compat import _check_path
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import NODE_CTOR_FSPATH_ARG from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
from _pytest.mark.structures import Mark from _pytest.mark.structures import Mark
@ -94,14 +97,6 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]:
yield nodeid 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( def _imply_path(
node_type: Type["Node"], node_type: Type["Node"],
path: Optional[Path], path: Optional[Path],
@ -264,7 +259,7 @@ class Node(metaclass=NodeMeta):
return cls._create(parent=parent, **kw) return cls._create(parent=parent, **kw)
@property @property
def ihook(self): def ihook(self) -> pluggy.HookRelay:
"""fspath-sensitive hook proxy used to call pytest hooks.""" """fspath-sensitive hook proxy used to call pytest hooks."""
return self.session.gethookproxy(self.path) return self.session.gethookproxy(self.path)