_manager.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. import inspect
  2. import sys
  3. import warnings
  4. from . import _tracing
  5. from ._callers import _Result, _multicall
  6. from ._hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts
  7. if sys.version_info >= (3, 8):
  8. from importlib import metadata as importlib_metadata
  9. else:
  10. import importlib_metadata
  11. def _warn_for_function(warning, function):
  12. warnings.warn_explicit(
  13. warning,
  14. type(warning),
  15. lineno=function.__code__.co_firstlineno,
  16. filename=function.__code__.co_filename,
  17. )
  18. class PluginValidationError(Exception):
  19. """plugin failed validation.
  20. :param object plugin: the plugin which failed validation,
  21. may be a module or an arbitrary object.
  22. """
  23. def __init__(self, plugin, message):
  24. self.plugin = plugin
  25. super(Exception, self).__init__(message)
  26. class DistFacade:
  27. """Emulate a pkg_resources Distribution"""
  28. def __init__(self, dist):
  29. self._dist = dist
  30. @property
  31. def project_name(self):
  32. return self.metadata["name"]
  33. def __getattr__(self, attr, default=None):
  34. return getattr(self._dist, attr, default)
  35. def __dir__(self):
  36. return sorted(dir(self._dist) + ["_dist", "project_name"])
  37. class PluginManager:
  38. """Core :py:class:`.PluginManager` class which manages registration
  39. of plugin objects and 1:N hook calling.
  40. You can register new hooks by calling :py:meth:`add_hookspecs(module_or_class)
  41. <.PluginManager.add_hookspecs>`.
  42. You can register plugin objects (which contain hooks) by calling
  43. :py:meth:`register(plugin) <.PluginManager.register>`. The :py:class:`.PluginManager`
  44. is initialized with a prefix that is searched for in the names of the dict
  45. of registered plugin objects.
  46. For debugging purposes you can call :py:meth:`.PluginManager.enable_tracing`
  47. which will subsequently send debug information to the trace helper.
  48. """
  49. def __init__(self, project_name):
  50. self.project_name = project_name
  51. self._name2plugin = {}
  52. self._plugin2hookcallers = {}
  53. self._plugin_distinfo = []
  54. self.trace = _tracing.TagTracer().get("pluginmanage")
  55. self.hook = _HookRelay()
  56. self._inner_hookexec = _multicall
  57. def _hookexec(self, hook_name, methods, kwargs, firstresult):
  58. # called from all hookcaller instances.
  59. # enable_tracing will set its own wrapping function at self._inner_hookexec
  60. return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  61. def register(self, plugin, name=None):
  62. """Register a plugin and return its canonical name or ``None`` if the name
  63. is blocked from registering. Raise a :py:class:`ValueError` if the plugin
  64. is already registered."""
  65. plugin_name = name or self.get_canonical_name(plugin)
  66. if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
  67. if self._name2plugin.get(plugin_name, -1) is None:
  68. return # blocked plugin, return None to indicate no registration
  69. raise ValueError(
  70. "Plugin already registered: %s=%s\n%s"
  71. % (plugin_name, plugin, self._name2plugin)
  72. )
  73. # XXX if an error happens we should make sure no state has been
  74. # changed at point of return
  75. self._name2plugin[plugin_name] = plugin
  76. # register matching hook implementations of the plugin
  77. self._plugin2hookcallers[plugin] = hookcallers = []
  78. for name in dir(plugin):
  79. hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
  80. if hookimpl_opts is not None:
  81. normalize_hookimpl_opts(hookimpl_opts)
  82. method = getattr(plugin, name)
  83. hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
  84. name = hookimpl_opts.get("specname") or name
  85. hook = getattr(self.hook, name, None)
  86. if hook is None:
  87. hook = _HookCaller(name, self._hookexec)
  88. setattr(self.hook, name, hook)
  89. elif hook.has_spec():
  90. self._verify_hook(hook, hookimpl)
  91. hook._maybe_apply_history(hookimpl)
  92. hook._add_hookimpl(hookimpl)
  93. hookcallers.append(hook)
  94. return plugin_name
  95. def parse_hookimpl_opts(self, plugin, name):
  96. method = getattr(plugin, name)
  97. if not inspect.isroutine(method):
  98. return
  99. try:
  100. res = getattr(method, self.project_name + "_impl", None)
  101. except Exception:
  102. res = {}
  103. if res is not None and not isinstance(res, dict):
  104. # false positive
  105. res = None
  106. return res
  107. def unregister(self, plugin=None, name=None):
  108. """unregister a plugin object and all its contained hook implementations
  109. from internal data structures."""
  110. if name is None:
  111. assert plugin is not None, "one of name or plugin needs to be specified"
  112. name = self.get_name(plugin)
  113. if plugin is None:
  114. plugin = self.get_plugin(name)
  115. # if self._name2plugin[name] == None registration was blocked: ignore
  116. if self._name2plugin.get(name):
  117. del self._name2plugin[name]
  118. for hookcaller in self._plugin2hookcallers.pop(plugin, []):
  119. hookcaller._remove_plugin(plugin)
  120. return plugin
  121. def set_blocked(self, name):
  122. """block registrations of the given name, unregister if already registered."""
  123. self.unregister(name=name)
  124. self._name2plugin[name] = None
  125. def is_blocked(self, name):
  126. """return ``True`` if the given plugin name is blocked."""
  127. return name in self._name2plugin and self._name2plugin[name] is None
  128. def add_hookspecs(self, module_or_class):
  129. """add new hook specifications defined in the given ``module_or_class``.
  130. Functions are recognized if they have been decorated accordingly."""
  131. names = []
  132. for name in dir(module_or_class):
  133. spec_opts = self.parse_hookspec_opts(module_or_class, name)
  134. if spec_opts is not None:
  135. hc = getattr(self.hook, name, None)
  136. if hc is None:
  137. hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
  138. setattr(self.hook, name, hc)
  139. else:
  140. # plugins registered this hook without knowing the spec
  141. hc.set_specification(module_or_class, spec_opts)
  142. for hookfunction in hc.get_hookimpls():
  143. self._verify_hook(hc, hookfunction)
  144. names.append(name)
  145. if not names:
  146. raise ValueError(
  147. f"did not find any {self.project_name!r} hooks in {module_or_class!r}"
  148. )
  149. def parse_hookspec_opts(self, module_or_class, name):
  150. method = getattr(module_or_class, name)
  151. return getattr(method, self.project_name + "_spec", None)
  152. def get_plugins(self):
  153. """return the set of registered plugins."""
  154. return set(self._plugin2hookcallers)
  155. def is_registered(self, plugin):
  156. """Return ``True`` if the plugin is already registered."""
  157. return plugin in self._plugin2hookcallers
  158. def get_canonical_name(self, plugin):
  159. """Return canonical name for a plugin object. Note that a plugin
  160. may be registered under a different name which was specified
  161. by the caller of :py:meth:`register(plugin, name) <.PluginManager.register>`.
  162. To obtain the name of an registered plugin use :py:meth:`get_name(plugin)
  163. <.PluginManager.get_name>` instead."""
  164. return getattr(plugin, "__name__", None) or str(id(plugin))
  165. def get_plugin(self, name):
  166. """Return a plugin or ``None`` for the given name."""
  167. return self._name2plugin.get(name)
  168. def has_plugin(self, name):
  169. """Return ``True`` if a plugin with the given name is registered."""
  170. return self.get_plugin(name) is not None
  171. def get_name(self, plugin):
  172. """Return name for registered plugin or ``None`` if not registered."""
  173. for name, val in self._name2plugin.items():
  174. if plugin == val:
  175. return name
  176. def _verify_hook(self, hook, hookimpl):
  177. if hook.is_historic() and hookimpl.hookwrapper:
  178. raise PluginValidationError(
  179. hookimpl.plugin,
  180. "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper"
  181. % (hookimpl.plugin_name, hook.name),
  182. )
  183. if hook.spec.warn_on_impl:
  184. _warn_for_function(hook.spec.warn_on_impl, hookimpl.function)
  185. # positional arg checking
  186. notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
  187. if notinspec:
  188. raise PluginValidationError(
  189. hookimpl.plugin,
  190. "Plugin %r for hook %r\nhookimpl definition: %s\n"
  191. "Argument(s) %s are declared in the hookimpl but "
  192. "can not be found in the hookspec"
  193. % (
  194. hookimpl.plugin_name,
  195. hook.name,
  196. _formatdef(hookimpl.function),
  197. notinspec,
  198. ),
  199. )
  200. if hookimpl.hookwrapper and not inspect.isgeneratorfunction(hookimpl.function):
  201. raise PluginValidationError(
  202. hookimpl.plugin,
  203. "Plugin %r for hook %r\nhookimpl definition: %s\n"
  204. "Declared as hookwrapper=True but function is not a generator function"
  205. % (hookimpl.plugin_name, hook.name, _formatdef(hookimpl.function)),
  206. )
  207. def check_pending(self):
  208. """Verify that all hooks which have not been verified against
  209. a hook specification are optional, otherwise raise :py:class:`.PluginValidationError`."""
  210. for name in self.hook.__dict__:
  211. if name[0] != "_":
  212. hook = getattr(self.hook, name)
  213. if not hook.has_spec():
  214. for hookimpl in hook.get_hookimpls():
  215. if not hookimpl.optionalhook:
  216. raise PluginValidationError(
  217. hookimpl.plugin,
  218. "unknown hook %r in plugin %r"
  219. % (name, hookimpl.plugin),
  220. )
  221. def load_setuptools_entrypoints(self, group, name=None):
  222. """Load modules from querying the specified setuptools ``group``.
  223. :param str group: entry point group to load plugins
  224. :param str name: if given, loads only plugins with the given ``name``.
  225. :rtype: int
  226. :return: return the number of loaded plugins by this call.
  227. """
  228. count = 0
  229. for dist in list(importlib_metadata.distributions()):
  230. for ep in dist.entry_points:
  231. if (
  232. ep.group != group
  233. or (name is not None and ep.name != name)
  234. # already registered
  235. or self.get_plugin(ep.name)
  236. or self.is_blocked(ep.name)
  237. ):
  238. continue
  239. plugin = ep.load()
  240. self.register(plugin, name=ep.name)
  241. self._plugin_distinfo.append((plugin, DistFacade(dist)))
  242. count += 1
  243. return count
  244. def list_plugin_distinfo(self):
  245. """return list of distinfo/plugin tuples for all setuptools registered
  246. plugins."""
  247. return list(self._plugin_distinfo)
  248. def list_name_plugin(self):
  249. """return list of name/plugin pairs."""
  250. return list(self._name2plugin.items())
  251. def get_hookcallers(self, plugin):
  252. """get all hook callers for the specified plugin."""
  253. return self._plugin2hookcallers.get(plugin)
  254. def add_hookcall_monitoring(self, before, after):
  255. """add before/after tracing functions for all hooks
  256. and return an undo function which, when called,
  257. will remove the added tracers.
  258. ``before(hook_name, hook_impls, kwargs)`` will be called ahead
  259. of all hook calls and receive a hookcaller instance, a list
  260. of HookImpl instances and the keyword arguments for the hook call.
  261. ``after(outcome, hook_name, hook_impls, kwargs)`` receives the
  262. same arguments as ``before`` but also a :py:class:`pluggy._callers._Result` object
  263. which represents the result of the overall hook call.
  264. """
  265. oldcall = self._inner_hookexec
  266. def traced_hookexec(hook_name, hook_impls, kwargs, firstresult):
  267. before(hook_name, hook_impls, kwargs)
  268. outcome = _Result.from_call(
  269. lambda: oldcall(hook_name, hook_impls, kwargs, firstresult)
  270. )
  271. after(outcome, hook_name, hook_impls, kwargs)
  272. return outcome.get_result()
  273. self._inner_hookexec = traced_hookexec
  274. def undo():
  275. self._inner_hookexec = oldcall
  276. return undo
  277. def enable_tracing(self):
  278. """enable tracing of hook calls and return an undo function."""
  279. hooktrace = self.trace.root.get("hook")
  280. def before(hook_name, methods, kwargs):
  281. hooktrace.root.indent += 1
  282. hooktrace(hook_name, kwargs)
  283. def after(outcome, hook_name, methods, kwargs):
  284. if outcome.excinfo is None:
  285. hooktrace("finish", hook_name, "-->", outcome.get_result())
  286. hooktrace.root.indent -= 1
  287. return self.add_hookcall_monitoring(before, after)
  288. def subset_hook_caller(self, name, remove_plugins):
  289. """Return a new :py:class:`._hooks._HookCaller` instance for the named method
  290. which manages calls to all registered plugins except the
  291. ones from remove_plugins."""
  292. orig = getattr(self.hook, name)
  293. plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
  294. if plugins_to_remove:
  295. hc = _HookCaller(
  296. orig.name, orig._hookexec, orig.spec.namespace, orig.spec.opts
  297. )
  298. for hookimpl in orig.get_hookimpls():
  299. plugin = hookimpl.plugin
  300. if plugin not in plugins_to_remove:
  301. hc._add_hookimpl(hookimpl)
  302. # we also keep track of this hook caller so it
  303. # gets properly removed on plugin unregistration
  304. self._plugin2hookcallers.setdefault(plugin, []).append(hc)
  305. return hc
  306. return orig
  307. def _formatdef(func):
  308. return f"{func.__name__}{inspect.signature(func)}"