_hooks.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. """
  2. Internal hook annotation, representation and calling machinery.
  3. """
  4. import inspect
  5. import sys
  6. import warnings
  7. class HookspecMarker:
  8. """Decorator helper class for marking functions as hook specifications.
  9. You can instantiate it with a project_name to get a decorator.
  10. Calling :py:meth:`.PluginManager.add_hookspecs` later will discover all marked functions
  11. if the :py:class:`.PluginManager` uses the same project_name.
  12. """
  13. def __init__(self, project_name):
  14. self.project_name = project_name
  15. def __call__(
  16. self, function=None, firstresult=False, historic=False, warn_on_impl=None
  17. ):
  18. """if passed a function, directly sets attributes on the function
  19. which will make it discoverable to :py:meth:`.PluginManager.add_hookspecs`.
  20. If passed no function, returns a decorator which can be applied to a function
  21. later using the attributes supplied.
  22. If ``firstresult`` is ``True`` the 1:N hook call (N being the number of registered
  23. hook implementation functions) will stop at I<=N when the I'th function
  24. returns a non-``None`` result.
  25. If ``historic`` is ``True`` calls to a hook will be memorized and replayed
  26. on later registered plugins.
  27. """
  28. def setattr_hookspec_opts(func):
  29. if historic and firstresult:
  30. raise ValueError("cannot have a historic firstresult hook")
  31. setattr(
  32. func,
  33. self.project_name + "_spec",
  34. dict(
  35. firstresult=firstresult,
  36. historic=historic,
  37. warn_on_impl=warn_on_impl,
  38. ),
  39. )
  40. return func
  41. if function is not None:
  42. return setattr_hookspec_opts(function)
  43. else:
  44. return setattr_hookspec_opts
  45. class HookimplMarker:
  46. """Decorator helper class for marking functions as hook implementations.
  47. You can instantiate with a ``project_name`` to get a decorator.
  48. Calling :py:meth:`.PluginManager.register` later will discover all marked functions
  49. if the :py:class:`.PluginManager` uses the same project_name.
  50. """
  51. def __init__(self, project_name):
  52. self.project_name = project_name
  53. def __call__(
  54. self,
  55. function=None,
  56. hookwrapper=False,
  57. optionalhook=False,
  58. tryfirst=False,
  59. trylast=False,
  60. specname=None,
  61. ):
  62. """if passed a function, directly sets attributes on the function
  63. which will make it discoverable to :py:meth:`.PluginManager.register`.
  64. If passed no function, returns a decorator which can be applied to a
  65. function later using the attributes supplied.
  66. If ``optionalhook`` is ``True`` a missing matching hook specification will not result
  67. in an error (by default it is an error if no matching spec is found).
  68. If ``tryfirst`` is ``True`` this hook implementation will run as early as possible
  69. in the chain of N hook implementations for a specification.
  70. If ``trylast`` is ``True`` this hook implementation will run as late as possible
  71. in the chain of N hook implementations.
  72. If ``hookwrapper`` is ``True`` the hook implementations needs to execute exactly
  73. one ``yield``. The code before the ``yield`` is run early before any non-hookwrapper
  74. function is run. The code after the ``yield`` is run after all non-hookwrapper
  75. function have run. The ``yield`` receives a :py:class:`.callers._Result` object
  76. representing the exception or result outcome of the inner calls (including other
  77. hookwrapper calls).
  78. If ``specname`` is provided, it will be used instead of the function name when
  79. matching this hook implementation to a hook specification during registration.
  80. """
  81. def setattr_hookimpl_opts(func):
  82. setattr(
  83. func,
  84. self.project_name + "_impl",
  85. dict(
  86. hookwrapper=hookwrapper,
  87. optionalhook=optionalhook,
  88. tryfirst=tryfirst,
  89. trylast=trylast,
  90. specname=specname,
  91. ),
  92. )
  93. return func
  94. if function is None:
  95. return setattr_hookimpl_opts
  96. else:
  97. return setattr_hookimpl_opts(function)
  98. def normalize_hookimpl_opts(opts):
  99. opts.setdefault("tryfirst", False)
  100. opts.setdefault("trylast", False)
  101. opts.setdefault("hookwrapper", False)
  102. opts.setdefault("optionalhook", False)
  103. opts.setdefault("specname", None)
  104. _PYPY = hasattr(sys, "pypy_version_info")
  105. def varnames(func):
  106. """Return tuple of positional and keywrord argument names for a function,
  107. method, class or callable.
  108. In case of a class, its ``__init__`` method is considered.
  109. For methods the ``self`` parameter is not included.
  110. """
  111. if inspect.isclass(func):
  112. try:
  113. func = func.__init__
  114. except AttributeError:
  115. return (), ()
  116. elif not inspect.isroutine(func): # callable object?
  117. try:
  118. func = getattr(func, "__call__", func)
  119. except Exception:
  120. return (), ()
  121. try: # func MUST be a function or method here or we won't parse any args
  122. spec = inspect.getfullargspec(func)
  123. except TypeError:
  124. return (), ()
  125. args, defaults = tuple(spec.args), spec.defaults
  126. if defaults:
  127. index = -len(defaults)
  128. args, kwargs = args[:index], tuple(args[index:])
  129. else:
  130. kwargs = ()
  131. # strip any implicit instance arg
  132. # pypy3 uses "obj" instead of "self" for default dunder methods
  133. implicit_names = ("self",) if not _PYPY else ("self", "obj")
  134. if args:
  135. if inspect.ismethod(func) or (
  136. "." in getattr(func, "__qualname__", ()) and args[0] in implicit_names
  137. ):
  138. args = args[1:]
  139. return args, kwargs
  140. class _HookRelay:
  141. """hook holder object for performing 1:N hook calls where N is the number
  142. of registered plugins.
  143. """
  144. class _HookCaller:
  145. def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
  146. self.name = name
  147. self._wrappers = []
  148. self._nonwrappers = []
  149. self._hookexec = hook_execute
  150. self._call_history = None
  151. self.spec = None
  152. if specmodule_or_class is not None:
  153. assert spec_opts is not None
  154. self.set_specification(specmodule_or_class, spec_opts)
  155. def has_spec(self):
  156. return self.spec is not None
  157. def set_specification(self, specmodule_or_class, spec_opts):
  158. assert not self.has_spec()
  159. self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
  160. if spec_opts.get("historic"):
  161. self._call_history = []
  162. def is_historic(self):
  163. return self._call_history is not None
  164. def _remove_plugin(self, plugin):
  165. def remove(wrappers):
  166. for i, method in enumerate(wrappers):
  167. if method.plugin == plugin:
  168. del wrappers[i]
  169. return True
  170. if remove(self._wrappers) is None:
  171. if remove(self._nonwrappers) is None:
  172. raise ValueError(f"plugin {plugin!r} not found")
  173. def get_hookimpls(self):
  174. # Order is important for _hookexec
  175. return self._nonwrappers + self._wrappers
  176. def _add_hookimpl(self, hookimpl):
  177. """Add an implementation to the callback chain."""
  178. if hookimpl.hookwrapper:
  179. methods = self._wrappers
  180. else:
  181. methods = self._nonwrappers
  182. if hookimpl.trylast:
  183. methods.insert(0, hookimpl)
  184. elif hookimpl.tryfirst:
  185. methods.append(hookimpl)
  186. else:
  187. # find last non-tryfirst method
  188. i = len(methods) - 1
  189. while i >= 0 and methods[i].tryfirst:
  190. i -= 1
  191. methods.insert(i + 1, hookimpl)
  192. def __repr__(self):
  193. return f"<_HookCaller {self.name!r}>"
  194. def __call__(self, *args, **kwargs):
  195. if args:
  196. raise TypeError("hook calling supports only keyword arguments")
  197. assert not self.is_historic()
  198. # This is written to avoid expensive operations when not needed.
  199. if self.spec:
  200. for argname in self.spec.argnames:
  201. if argname not in kwargs:
  202. notincall = tuple(set(self.spec.argnames) - kwargs.keys())
  203. warnings.warn(
  204. "Argument(s) {} which are declared in the hookspec "
  205. "can not be found in this hook call".format(notincall),
  206. stacklevel=2,
  207. )
  208. break
  209. firstresult = self.spec.opts.get("firstresult")
  210. else:
  211. firstresult = False
  212. return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
  213. def call_historic(self, result_callback=None, kwargs=None):
  214. """Call the hook with given ``kwargs`` for all registered plugins and
  215. for all plugins which will be registered afterwards.
  216. If ``result_callback`` is not ``None`` it will be called for for each
  217. non-``None`` result obtained from a hook implementation.
  218. """
  219. self._call_history.append((kwargs or {}, result_callback))
  220. # Historizing hooks don't return results.
  221. # Remember firstresult isn't compatible with historic.
  222. res = self._hookexec(self.name, self.get_hookimpls(), kwargs, False)
  223. if result_callback is None:
  224. return
  225. for x in res or []:
  226. result_callback(x)
  227. def call_extra(self, methods, kwargs):
  228. """Call the hook with some additional temporarily participating
  229. methods using the specified ``kwargs`` as call parameters."""
  230. old = list(self._nonwrappers), list(self._wrappers)
  231. for method in methods:
  232. opts = dict(hookwrapper=False, trylast=False, tryfirst=False)
  233. hookimpl = HookImpl(None, "<temp>", method, opts)
  234. self._add_hookimpl(hookimpl)
  235. try:
  236. return self(**kwargs)
  237. finally:
  238. self._nonwrappers, self._wrappers = old
  239. def _maybe_apply_history(self, method):
  240. """Apply call history to a new hookimpl if it is marked as historic."""
  241. if self.is_historic():
  242. for kwargs, result_callback in self._call_history:
  243. res = self._hookexec(self.name, [method], kwargs, False)
  244. if res and result_callback is not None:
  245. result_callback(res[0])
  246. class HookImpl:
  247. def __init__(self, plugin, plugin_name, function, hook_impl_opts):
  248. self.function = function
  249. self.argnames, self.kwargnames = varnames(self.function)
  250. self.plugin = plugin
  251. self.opts = hook_impl_opts
  252. self.plugin_name = plugin_name
  253. self.__dict__.update(hook_impl_opts)
  254. def __repr__(self):
  255. return f"<HookImpl plugin_name={self.plugin_name!r}, plugin={self.plugin!r}>"
  256. class HookSpec:
  257. def __init__(self, namespace, name, opts):
  258. self.namespace = namespace
  259. self.function = function = getattr(namespace, name)
  260. self.name = name
  261. self.argnames, self.kwargnames = varnames(function)
  262. self.opts = opts
  263. self.warn_on_impl = opts.get("warn_on_impl")