You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

359 lines
12 KiB

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