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