123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- 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
- 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]
- def _warn_for_function(warning: Warning, function: Callable[..., object]) -> None:
- func = cast(types.FunctionType, function)
- warnings.warn_explicit(
- warning,
- type(warning),
- lineno=func.__code__.co_firstlineno,
- filename=func.__code__.co_filename,
- )
- class PluginValidationError(Exception):
- """Plugin failed validation.
- :param plugin: The plugin which failed validation.
- :param message: Error message.
- """
- def __init__(self, plugin: _Plugin, message: str) -> None:
- super().__init__(message)
- #: The plugin which failed validation.
- 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=None):
- 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.
- You can register new hooks by calling :meth:`add_hookspecs(module_or_class)
- <PluginManager.add_hookspecs>`.
- You can register plugin objects (which contain hook implementations) by
- calling :meth:`register(plugin) <PluginManager.register>`.
- For debugging purposes you can call :meth:`PluginManager.enable_tracing`
- which will subsequently send debug information to the trace helper.
- :param project_name:
- The short project name. Prefer snake case. Make sure it's unique!
- """
- 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]]] = []
- #: The "hook relay", used to call a hook on all registered plugins.
- #: See :ref:`calling`.
- self.hook: Final = HookRelay()
- #: The tracing entry point. See :ref:`tracing`.
- self.trace: Final[_tracing.TagTracerSub] = _tracing.TagTracer().get(
- "pluginmanage"
- )
- self._inner_hookexec = _multicall
- def _hookexec(
- self,
- hook_name: str,
- methods: Sequence[HookImpl],
- kwargs: Mapping[str, object],
- firstresult: bool,
- ) -> object | list[object]:
- # called from all hookcaller instances.
- # enable_tracing will set its own wrapping function at self._inner_hookexec
- return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
- def register(self, plugin: _Plugin, name: str | None = None) -> str | None:
- """Register a plugin and return its name.
- :param name:
- The name under which to register the plugin. If not specified, a
- name is generated using :func:`get_canonical_name`.
- :returns:
- The plugin name. If the name is blocked from registering, returns
- ``None``.
- If the plugin is already registered, raises a :exc:`ValueError`.
- """
- plugin_name = name or self.get_canonical_name(plugin)
- if plugin_name in self._name2plugin:
- if self._name2plugin.get(plugin_name, -1) is None:
- return None # blocked plugin, return None to indicate no registration
- raise ValueError(
- "Plugin name already registered: %s=%s\n%s"
- % (plugin_name, plugin, self._name2plugin)
- )
- if plugin in self._name2plugin.values():
- raise ValueError(
- "Plugin already registered under a different name: %s=%s\n%s"
- % (plugin_name, plugin, self._name2plugin)
- )
- # XXX if an error happens we should make sure no state has been
- # changed at point of return
- self._name2plugin[plugin_name] = plugin
- # register matching hook implementations of the plugin
- for name in dir(plugin):
- hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
- if hookimpl_opts is not None:
- normalize_hookimpl_opts(hookimpl_opts)
- method: _HookImplFunction[object] = getattr(plugin, name)
- hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
- name = hookimpl_opts.get("specname") or name
- hook: HookCaller | None = getattr(self.hook, name, None)
- if hook is None:
- hook = HookCaller(name, self._hookexec)
- setattr(self.hook, name, hook)
- elif hook.has_spec():
- self._verify_hook(hook, hookimpl)
- hook._maybe_apply_history(hookimpl)
- hook._add_hookimpl(hookimpl)
- return plugin_name
- def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None:
- """Try to obtain a hook implementation from an item with the given name
- in the given plugin which is being searched for hook impls.
- :returns:
- The parsed hookimpl options, or None to skip the given item.
- This method can be overridden by ``PluginManager`` subclasses to
- customize how hook implementation are picked up. By default, returns the
- options for items decorated with :class:`HookimplMarker`.
- """
- method: object = getattr(plugin, name)
- if not inspect.isroutine(method):
- return None
- try:
- res: HookimplOpts | None = getattr(
- method, self.project_name + "_impl", None
- )
- except Exception:
- res = {} # type: ignore[assignment]
- if res is not None and not isinstance(res, dict):
- # false positive
- res = None # type:ignore[unreachable]
- return res
- def unregister(
- self, plugin: _Plugin | None = None, name: str | None = None
- ) -> Any | None:
- """Unregister a plugin and all of its hook implementations.
- The plugin can be specified either by the plugin object or the plugin
- name. If both are specified, they must agree.
- Returns the unregistered plugin, or ``None`` if not found.
- """
- if name is None:
- assert plugin is not None, "one of name or plugin needs to be specified"
- name = self.get_name(plugin)
- assert name is not None, "plugin is not registered"
- if plugin is None:
- plugin = self.get_plugin(name)
- if plugin is None:
- return None
- hookcallers = self.get_hookcallers(plugin)
- if hookcallers:
- for hookcaller in hookcallers:
- hookcaller._remove_plugin(plugin)
- # if self._name2plugin[name] == None registration was blocked: ignore
- if self._name2plugin.get(name):
- assert name is not None
- del self._name2plugin[name]
- return plugin
- def set_blocked(self, name: str) -> None:
- """Block registrations of the given name, unregister if already registered."""
- self.unregister(name=name)
- self._name2plugin[name] = None
- def is_blocked(self, name: str) -> bool:
- """Return whether the given plugin name is blocked."""
- return name in self._name2plugin and self._name2plugin[name] is None
- def unblock(self, name: str) -> bool:
- """Unblocks a name.
- Returns whether the name was actually blocked.
- """
- if self._name2plugin.get(name, -1) is None:
- del self._name2plugin[name]
- return True
- return False
- def add_hookspecs(self, module_or_class: _Namespace) -> None:
- """Add new hook specifications defined in the given ``module_or_class``.
- Functions are recognized as hook specifications if they have been
- decorated with a matching :class:`HookspecMarker`.
- """
- names = []
- for name in dir(module_or_class):
- spec_opts = self.parse_hookspec_opts(module_or_class, name)
- if spec_opts is not None:
- hc: HookCaller | None = getattr(self.hook, name, None)
- if hc is None:
- hc = HookCaller(name, self._hookexec, module_or_class, spec_opts)
- setattr(self.hook, name, hc)
- else:
- # Plugins registered this hook without knowing the spec.
- hc.set_specification(module_or_class, spec_opts)
- for hookfunction in hc.get_hookimpls():
- self._verify_hook(hc, hookfunction)
- names.append(name)
- if not names:
- raise ValueError(
- f"did not find any {self.project_name!r} hooks in {module_or_class!r}"
- )
- def parse_hookspec_opts(
- self, module_or_class: _Namespace, name: str
- ) -> HookspecOpts | None:
- """Try to obtain a hook specification from an item with the given name
- in the given module or class which is being searched for hook specs.
- :returns:
- The parsed hookspec options for defining a hook, or None to skip the
- given item.
- This method can be overridden by ``PluginManager`` subclasses to
- customize how hook specifications are picked up. By default, returns the
- options for items decorated with :class:`HookspecMarker`.
- """
- method = getattr(module_or_class, name)
- opts: HookspecOpts | None = getattr(method, self.project_name + "_spec", None)
- return opts
- def get_plugins(self) -> set[Any]:
- """Return a set of all registered plugin objects."""
- return {x for x in self._name2plugin.values() if x is not None}
- def is_registered(self, plugin: _Plugin) -> bool:
- """Return whether the plugin is already registered."""
- return any(plugin == val for val in self._name2plugin.values())
- def get_canonical_name(self, plugin: _Plugin) -> str:
- """Return a canonical name for a plugin object.
- Note that a plugin may be registered under a different name
- specified by the caller of :meth:`register(plugin, name) <register>`.
- To obtain the name of a registered plugin use :meth:`get_name(plugin)
- <get_name>` instead.
- """
- name: str | None = getattr(plugin, "__name__", None)
- return name or str(id(plugin))
- def get_plugin(self, name: str) -> Any | None:
- """Return the plugin registered under the given name, if any."""
- return self._name2plugin.get(name)
- def has_plugin(self, name: str) -> bool:
- """Return whether a plugin with the given name is registered."""
- return self.get_plugin(name) is not None
- def get_name(self, plugin: _Plugin) -> str | None:
- """Return the name the plugin is registered under, or ``None`` if
- is isn't."""
- for name, val in self._name2plugin.items():
- if plugin == val:
- return name
- return None
- def _verify_hook(self, hook: HookCaller, hookimpl: HookImpl) -> None:
- if hook.is_historic() and (hookimpl.hookwrapper or hookimpl.wrapper):
- raise PluginValidationError(
- hookimpl.plugin,
- "Plugin %r\nhook %r\nhistoric incompatible with yield/wrapper/hookwrapper"
- % (hookimpl.plugin_name, hook.name),
- )
- assert hook.spec is not None
- if hook.spec.warn_on_impl:
- _warn_for_function(hook.spec.warn_on_impl, hookimpl.function)
- # positional arg checking
- notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
- if notinspec:
- raise PluginValidationError(
- hookimpl.plugin,
- "Plugin %r for hook %r\nhookimpl definition: %s\n"
- "Argument(s) %s are declared in the hookimpl but "
- "can not be found in the hookspec"
- % (
- hookimpl.plugin_name,
- hook.name,
- _formatdef(hookimpl.function),
- notinspec,
- ),
- )
- if hook.spec.warn_on_impl_args:
- for hookimpl_argname in hookimpl.argnames:
- argname_warning = hook.spec.warn_on_impl_args.get(hookimpl_argname)
- if argname_warning is not None:
- _warn_for_function(argname_warning, hookimpl.function)
- if (
- hookimpl.wrapper or hookimpl.hookwrapper
- ) and not inspect.isgeneratorfunction(hookimpl.function):
- raise PluginValidationError(
- hookimpl.plugin,
- "Plugin %r for hook %r\nhookimpl definition: %s\n"
- "Declared as wrapper=True or hookwrapper=True "
- "but function is not a generator function"
- % (hookimpl.plugin_name, hook.name, _formatdef(hookimpl.function)),
- )
- if hookimpl.wrapper and hookimpl.hookwrapper:
- raise PluginValidationError(
- hookimpl.plugin,
- "Plugin %r for hook %r\nhookimpl definition: %s\n"
- "The wrapper=True and hookwrapper=True options are mutually exclusive"
- % (hookimpl.plugin_name, hook.name, _formatdef(hookimpl.function)),
- )
- def check_pending(self) -> None:
- """Verify that all hooks which have not been verified against a
- hook specification are optional, otherwise raise
- :exc:`PluginValidationError`."""
- for name in self.hook.__dict__:
- if name[0] != "_":
- hook: HookCaller = getattr(self.hook, name)
- if not hook.has_spec():
- for hookimpl in hook.get_hookimpls():
- if not hookimpl.optionalhook:
- raise PluginValidationError(
- hookimpl.plugin,
- "unknown hook %r in plugin %r"
- % (name, hookimpl.plugin),
- )
- def load_setuptools_entrypoints(self, 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.
- """
- 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
- 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)
- def list_name_plugin(self) -> list[tuple[str, _Plugin]]:
- """Return a list of (name, plugin) pairs for all registered plugins."""
- return list(self._name2plugin.items())
- def get_hookcallers(self, plugin: _Plugin) -> list[HookCaller] | None:
- """Get all hook callers for the specified plugin.
- :returns:
- The hook callers, or ``None`` if ``plugin`` is not registered in
- this plugin manager.
- """
- if self.get_name(plugin) is None:
- return None
- hookcallers = []
- for hookcaller in self.hook.__dict__.values():
- for hookimpl in hookcaller.get_hookimpls():
- if hookimpl.plugin is plugin:
- hookcallers.append(hookcaller)
- return hookcallers
- def add_hookcall_monitoring(
- self, before: _BeforeTrace, after: _AfterTrace
- ) -> Callable[[], None]:
- """Add before/after tracing functions for all hooks.
- Returns an undo function which, when called, removes the added tracers.
- ``before(hook_name, hook_impls, kwargs)`` will be called ahead
- of all hook calls and receive a hookcaller instance, a list
- of HookImpl instances and the keyword arguments for the hook call.
- ``after(outcome, hook_name, hook_impls, kwargs)`` receives the
- same arguments as ``before`` but also a :class:`~pluggy.Result` object
- which represents the result of the overall hook call.
- """
- oldcall = self._inner_hookexec
- def traced_hookexec(
- hook_name: str,
- hook_impls: Sequence[HookImpl],
- caller_kwargs: Mapping[str, object],
- firstresult: bool,
- ) -> object | list[object]:
- before(hook_name, hook_impls, caller_kwargs)
- outcome = Result.from_call(
- lambda: oldcall(hook_name, hook_impls, caller_kwargs, firstresult)
- )
- after(outcome, hook_name, hook_impls, caller_kwargs)
- return outcome.get_result()
- self._inner_hookexec = traced_hookexec
- def undo() -> None:
- self._inner_hookexec = oldcall
- return undo
- def enable_tracing(self) -> Callable[[], None]:
- """Enable tracing of hook calls.
- Returns an undo function which, when called, removes the added tracing.
- """
- hooktrace = self.trace.root.get("hook")
- def before(
- hook_name: str, methods: Sequence[HookImpl], kwargs: Mapping[str, object]
- ) -> None:
- hooktrace.root.indent += 1
- hooktrace(hook_name, kwargs)
- def after(
- outcome: Result[object],
- hook_name: str,
- methods: Sequence[HookImpl],
- kwargs: Mapping[str, object],
- ) -> None:
- if outcome.exception is None:
- hooktrace("finish", hook_name, "-->", outcome.get_result())
- hooktrace.root.indent -= 1
- return self.add_hookcall_monitoring(before, after)
- def subset_hook_caller(
- self, name: str, remove_plugins: Iterable[_Plugin]
- ) -> HookCaller:
- """Return a proxy :class:`~pluggy.HookCaller` instance for the named
- method which manages calls to all registered plugins except the ones
- from remove_plugins."""
- orig: HookCaller = getattr(self.hook, name)
- plugins_to_remove = {plug for plug in remove_plugins if hasattr(plug, name)}
- if plugins_to_remove:
- return _SubsetHookCaller(orig, plugins_to_remove)
- return orig
- def _formatdef(func: Callable[..., object]) -> str:
- return f"{func.__name__}{inspect.signature(func)}"
|