Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

395 строки
15KB

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