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.

360 lines
12KB

  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")