manager.py 14 KB

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