diff --git a/src/pluggy/__init__.py b/src/pluggy/__init__.py index 36ce1680..8aacb0bc 100644 --- a/src/pluggy/__init__.py +++ b/src/pluggy/__init__.py @@ -25,13 +25,25 @@ from ._hooks import HookCaller from ._hooks import HookImpl from ._hooks import HookimplMarker -from ._hooks import HookimplOpts from ._hooks import HookRelay from ._hooks import HookspecMarker -from ._hooks import HookspecOpts from ._manager import PluginManager from ._manager import PluginValidationError from ._result import HookCallError from ._result import Result from ._warnings import PluggyTeardownRaisedWarning from ._warnings import PluggyWarning + + +TYPE_CHECKING = False +if TYPE_CHECKING: + from ._types import HookimplOpts + from ._types import HookspecOpts +else: + + def __getattr__(name: str) -> object: + if name.endswith("Opts"): + from . import _types + + return getattr(_types, name) + raise AttributeError(name) diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index f4a2aced..7759f1fa 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -4,13 +4,6 @@ from __future__ import annotations -from typing import cast -from typing import Generator -from typing import Mapping -from typing import NoReturn -from typing import Sequence -from typing import Tuple -from typing import Union import warnings from ._hooks import HookImpl @@ -19,12 +12,26 @@ from ._warnings import PluggyTeardownRaisedWarning -# Need to distinguish between old- and new-style hook wrappers. -# Wrapping with a tuple is the fastest type-safe way I found to do it. -Teardown = Union[ - Tuple[Generator[None, Result[object], None], HookImpl], - Generator[None, object, object], -] +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import cast + from typing import Generator + from typing import Mapping + from typing import NoReturn + from typing import Sequence + from typing import Tuple + from typing import Union + + # Need to distinguish between old- and new-style hook wrappers. + # Wrapping with a tuple is the fastest type-safe way I found to do it. + Teardown = Union[ + Tuple[Generator[None, Result[object], None], HookImpl], + Generator[None, object, object], + ] +else: + + def cast(t, v): + return v def _raise_wrapfail( @@ -84,7 +91,7 @@ def _multicall( # If this cast is not valid, a type error is raised below, # which is the desired response. res = hook_impl.function(*args) - wrapper_gen = cast(Generator[None, Result[object], None], res) + wrapper_gen = cast("Generator[None, Result[object], None]", res) next(wrapper_gen) # first yield teardowns.append((wrapper_gen, hook_impl)) except StopIteration: @@ -94,7 +101,7 @@ def _multicall( # If this cast is not valid, a type error is raised below, # which is the desired response. res = hook_impl.function(*args) - function_gen = cast(Generator[None, object, object], res) + function_gen = cast("Generator[None, object, object]", res) next(function_gen) # first yield teardowns.append(function_gen) except StopIteration: diff --git a/src/pluggy/_hooks.py b/src/pluggy/_hooks.py index a81d2b28..bb442ec5 100644 --- a/src/pluggy/_hooks.py +++ b/src/pluggy/_hooks.py @@ -4,76 +4,51 @@ from __future__ import annotations -import inspect import sys -from types import ModuleType -from typing import AbstractSet -from typing import Any -from typing import Callable -from typing import Final -from typing import final -from typing import Generator -from typing import List -from typing import Mapping -from typing import Optional -from typing import overload -from typing import Sequence -from typing import Tuple -from typing import TYPE_CHECKING -from typing import TypedDict -from typing import TypeVar -from typing import Union import warnings -from ._result import Result - -_T = TypeVar("_T") -_F = TypeVar("_F", bound=Callable[..., object]) -_Namespace = Union[ModuleType, type] _Plugin = object -_HookExec = Callable[ - [str, Sequence["HookImpl"], Mapping[str, object], bool], - Union[object, List[object]], -] -_HookImplFunction = Callable[..., Union[_T, Generator[None, Result[_T], None]]] - - -class HookspecOpts(TypedDict): - """Options for a hook specification.""" - - #: Whether the hook is :ref:`first result only `. - firstresult: bool - #: Whether the hook is :ref:`historic `. - historic: bool - #: Whether the hook :ref:`warns when implemented `. - warn_on_impl: Warning | None - #: Whether the hook warns when :ref:`certain arguments are requested - #: `. - #: - #: .. versionadded:: 1.5 - warn_on_impl_args: Mapping[str, Warning] | None - - -class HookimplOpts(TypedDict): - """Options for a hook implementation.""" - - #: Whether the hook implementation is a :ref:`wrapper `. - wrapper: bool - #: Whether the hook implementation is an :ref:`old-style wrapper - #: `. - hookwrapper: bool - #: Whether validation against a hook specification is :ref:`optional - #: `. - optionalhook: bool - #: Whether to try to order this hook implementation :ref:`first - #: `. - tryfirst: bool - #: Whether to try to order this hook implementation :ref:`last - #: `. - trylast: bool - #: The name of the hook specification to match, see :ref:`specname`. - specname: str | None + +TYPE_CHECKING = False +if TYPE_CHECKING: + from types import ModuleType + from typing import AbstractSet + from typing import Any + from typing import Callable + from typing import Final + from typing import final + from typing import Generator + from typing import List + from typing import Mapping + from typing import Optional + from typing import overload + from typing import Sequence + from typing import Tuple + from typing import TYPE_CHECKING + from typing import TypeVar + from typing import Union + + from ._result import Result + + _T = TypeVar("_T") + _F = TypeVar("_F", bound=Callable[..., object]) + _Namespace = Union[ModuleType, type] + _HookExec = Callable[ + [str, Sequence["HookImpl"], Mapping[str, object], bool], + Union[object, List[object]], + ] + _HookImplFunction = Callable[..., Union[_T, Generator[None, Result[_T], None]]] + _CallHistory = List[Tuple[Mapping[str, object], Optional[Callable[[Any], None]]]] + + from ._types import HookimplOpts + from ._types import HookspecOpts +else: + + def final(func: _F) -> _F: + return func + + overload = final @final @@ -299,6 +274,8 @@ def varnames(func: object) -> tuple[tuple[str, ...], tuple[str, ...]]: In case of a class, its ``__init__`` method is considered. For methods the ``self`` parameter is not included. """ + import inspect + if inspect.isclass(func): try: func = func.__init__ @@ -376,9 +353,6 @@ def __getattr__(self, name: str) -> HookCaller: ... _HookRelay = HookRelay -_CallHistory = List[Tuple[Mapping[str, object], Optional[Callable[[Any], None]]]] - - class HookCaller: """A caller of all registered implementations of a hook specification.""" diff --git a/src/pluggy/_importlib_metadata.py b/src/pluggy/_importlib_metadata.py new file mode 100644 index 00000000..c3b27a33 --- /dev/null +++ b/src/pluggy/_importlib_metadata.py @@ -0,0 +1,69 @@ +"""this module contains importlib_metadata usage and importing + +it's deferred to avoid import-time dependency on importlib_metadata + +.. code-block:: console + + python -X importtime -c 'import pluggy' 2> import0.log + tuna import0.log + + +""" + +from __future__ import annotations + +import importlib.metadata +from typing import Any + +from . import _manager + + +class DistFacade: + """Emulate a pkg_resources Distribution""" + + def __init__(self, dist: importlib.metadata.Distribution) -> None: + self._dist = dist + + @property + def project_name(self) -> str: + name: str = self.metadata["name"] + return name + + def __getattr__(self, attr: str, default: object | None = None) -> Any: + return getattr(self._dist, attr, default) + + def __dir__(self) -> list[str]: + return sorted(dir(self._dist) + ["_dist", "project_name"]) + + +def load_importlib_entrypoints( + manager: _manager.PluginManager, + group: str, + name: str | None = None, +) -> int: + """Load modules from querying the specified setuptools ``group``. + + :param group: + Entry point group to load plugins. + :param name: + If given, loads only plugins with the given ``name``. + + :return: + The number of plugins loaded by this call. + """ + count = 0 + for dist in list(importlib.metadata.distributions()): + for ep in dist.entry_points: + if ( + ep.group != group + or (name is not None and ep.name != name) + # already registered + or manager.get_plugin(ep.name) + or manager.is_blocked(ep.name) + ): + continue + plugin = ep.load() + manager.register(plugin, name=ep.name) + manager._plugin_dist_metadata.append((plugin, dist)) + count += 1 + return count diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index d778334b..10c16ef6 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -1,42 +1,45 @@ from __future__ import annotations -import inspect import types -from typing import Any -from typing import Callable -from typing import cast -from typing import Final -from typing import Iterable -from typing import Mapping -from typing import Sequence -from typing import TYPE_CHECKING import warnings from . import _tracing from ._callers import _multicall -from ._hooks import _HookImplFunction -from ._hooks import _Namespace from ._hooks import _Plugin from ._hooks import _SubsetHookCaller from ._hooks import HookCaller from ._hooks import HookImpl -from ._hooks import HookimplOpts from ._hooks import HookRelay -from ._hooks import HookspecOpts from ._hooks import normalize_hookimpl_opts from ._result import Result +TYPE_CHECKING = False if TYPE_CHECKING: - # importtlib.metadata import is slow, defer it. - import importlib.metadata - - -_BeforeTrace = Callable[[str, Sequence[HookImpl], Mapping[str, Any]], None] -_AfterTrace = Callable[[Result[Any], str, Sequence[HookImpl], Mapping[str, Any]], None] + from importlib.metadata import Distribution + from typing import Any + from typing import Callable + from typing import Final + from typing import Iterable + from typing import Mapping + from typing import Sequence + from typing import TypeAlias + + from ._hooks import _HookImplFunction + from ._hooks import _Namespace + from ._importlib_metadata import DistFacade + from ._types import HookimplOpts + from ._types import HookspecOpts + +_BeforeTrace: TypeAlias = "Callable[[str, Sequence[HookImpl], Mapping[str, Any]], None]" +_AfterTrace: TypeAlias = ( + "Callable[[Result[Any], str, Sequence[HookImpl], Mapping[str, Any]], None]" +) def _warn_for_function(warning: Warning, function: Callable[..., object]) -> None: + from typing import cast + func = cast(types.FunctionType, function) warnings.warn_explicit( warning, @@ -59,24 +62,6 @@ def __init__(self, plugin: _Plugin, message: str) -> None: self.plugin = plugin -class DistFacade: - """Emulate a pkg_resources Distribution""" - - def __init__(self, dist: importlib.metadata.Distribution) -> None: - self._dist = dist - - @property - def project_name(self) -> str: - name: str = self.metadata["name"] - return name - - def __getattr__(self, attr: str, default: Any | None = None) -> Any: - return getattr(self._dist, attr, default) - - def __dir__(self) -> list[str]: - return sorted(dir(self._dist) + ["_dist", "project_name"]) - - class PluginManager: """Core class which manages registration of plugin objects and 1:N hook calling. @@ -98,7 +83,7 @@ def __init__(self, project_name: str) -> None: #: The project name. self.project_name: Final = project_name self._name2plugin: Final[dict[str, _Plugin]] = {} - self._plugin_distinfo: Final[list[tuple[_Plugin, DistFacade]]] = [] + self._plugin_dist_metadata: Final[list[tuple[_Plugin, Distribution]]] = [] #: The "hook relay", used to call a hook on all registered plugins. #: See :ref:`calling`. self.hook: Final = HookRelay() @@ -182,6 +167,8 @@ def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None options for items decorated with :class:`HookimplMarker`. """ method: object = getattr(plugin, name) + import inspect + if not inspect.isroutine(method): return None try: @@ -347,6 +334,7 @@ def _verify_hook(self, hook: HookCaller, hookimpl: HookImpl) -> None: f"Argument(s) {notinspec} are declared in the hookimpl but " "can not be found in the hookspec", ) + import inspect if hook.spec.warn_on_impl_args: for hookimpl_argname in hookimpl.argnames: @@ -389,7 +377,11 @@ def check_pending(self) -> None: ) def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> int: - """Load modules from querying the specified setuptools ``group``. + """legacy alias for load_importlib_entrypoints""" + return self.load_importlib_entrypoints(group, name) + + def load_importlib_entrypoints(self, group: str, name: str | None = None) -> int: + """Load modules for the given importlib_metadata entrypoint ``group``. :param group: Entry point group to load plugins. @@ -399,29 +391,25 @@ def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> in :return: The number of plugins loaded by this call. """ - import importlib.metadata - - count = 0 - for dist in list(importlib.metadata.distributions()): - for ep in dist.entry_points: - if ( - ep.group != group - or (name is not None and ep.name != name) - # already registered - or self.get_plugin(ep.name) - or self.is_blocked(ep.name) - ): - continue - plugin = ep.load() - self.register(plugin, name=ep.name) - self._plugin_distinfo.append((plugin, DistFacade(dist))) - count += 1 - return count + from ._importlib_metadata import load_importlib_entrypoints + + return load_importlib_entrypoints(self, group, name) def list_plugin_distinfo(self) -> list[tuple[_Plugin, DistFacade]]: """Return a list of (plugin, distinfo) pairs for all - setuptools-registered plugins.""" - return list(self._plugin_distinfo) + setuptools-registered plugins. + + (soft deprecated) + """ + from ._importlib_metadata import DistFacade + + return [ + (plugin, DistFacade(metadata)) + for plugin, metadata in self._plugin_dist_metadata + ] + + def list_plugin_distributions(self) -> list[tuple[_Plugin, Distribution]]: + return self._plugin_dist_metadata[:] def list_name_plugin(self) -> list[tuple[str, _Plugin]]: """Return a list of (name, plugin) pairs for all registered plugins.""" @@ -519,4 +507,6 @@ def subset_hook_caller( def _formatdef(func: Callable[..., object]) -> str: + import inspect + return f"{func.__name__}{inspect.signature(func)}" diff --git a/src/pluggy/_result.py b/src/pluggy/_result.py index aa3912c0..d9e050f4 100644 --- a/src/pluggy/_result.py +++ b/src/pluggy/_result.py @@ -4,19 +4,34 @@ from __future__ import annotations -from types import TracebackType -from typing import Callable -from typing import cast -from typing import final -from typing import Generic -from typing import Optional -from typing import Tuple -from typing import Type -from typing import TypeVar +TYPE_CHECKING = False +if TYPE_CHECKING: + from types import TracebackType + from typing import Callable + from typing import cast + from typing import final + from typing import Generic + from typing import Optional + from typing import Tuple + from typing import Type + from typing import TypeVar -_ExcInfo = Tuple[Type[BaseException], BaseException, Optional[TracebackType]] -ResultType = TypeVar("ResultType") + _ExcInfo = Tuple[Type[BaseException], BaseException, Optional[TracebackType]] + ResultType = TypeVar("ResultType") +else: + from ._hooks import final + + def cast(v, t): + return t + + class Generic: + """fake generic""" + + def __class_getitem__(cls, key) -> type[object]: + return object + + ResultType = "ResultType" class HookCallError(Exception): @@ -100,7 +115,7 @@ def get_result(self) -> ResultType: exc = self._exception tb = self._traceback if exc is None: - return cast(ResultType, self._result) + return cast("ResultType", self._result) else: raise exc.with_traceback(tb) diff --git a/src/pluggy/_tracing.py b/src/pluggy/_tracing.py index cd238ad7..7cddc35c 100644 --- a/src/pluggy/_tracing.py +++ b/src/pluggy/_tracing.py @@ -4,14 +4,18 @@ from __future__ import annotations -from typing import Any -from typing import Callable -from typing import Sequence -from typing import Tuple +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any + from typing import Callable + from typing import Sequence + from typing import Tuple + from typing import TypeAlias -_Writer = Callable[[str], object] -_Processor = Callable[[Tuple[str, ...], Tuple[Any, ...]], object] + +_Writer: TypeAlias = "Callable[[str], object]" +_Processor: TypeAlias = "Callable[[Tuple[str, ...], Tuple[Any, ...]], object]" class TagTracer: diff --git a/src/pluggy/_types.py b/src/pluggy/_types.py new file mode 100644 index 00000000..41fd914f --- /dev/null +++ b/src/pluggy/_types.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import Mapping +from typing import TypedDict +import warnings + + +warnings.warn(ImportWarning(f"{__name__} imported outside of type checking")) + + +class HookspecOpts(TypedDict): + """Options for a hook specification.""" + + #: Whether the hook is :ref:`first result only `. + firstresult: bool + #: Whether the hook is :ref:`historic `. + historic: bool + #: Whether the hook :ref:`warns when implemented `. + warn_on_impl: Warning | None + #: Whether the hook warns when :ref:`certain arguments are requested + #: `. + #: + #: .. versionadded:: 1.5 + warn_on_impl_args: Mapping[str, Warning] | None + + +class HookimplOpts(TypedDict): + """Options for a hook implementation.""" + + #: Whether the hook implementation is a :ref:`wrapper `. + wrapper: bool + #: Whether the hook implementation is an :ref:`old-style wrapper + #: `. + hookwrapper: bool + #: Whether validation against a hook specification is :ref:`optional + #: `. + optionalhook: bool + #: Whether to try to order this hook implementation :ref:`first + #: `. + tryfirst: bool + #: Whether to try to order this hook implementation :ref:`last + #: `. + trylast: bool + #: The name of the hook specification to match, see :ref:`specname`. + specname: str | None