From 11955b7765b78bb1cd0aa36d0d1c255a9c999459 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2016 15:39:33 -0500 Subject: [PATCH] Future warn about hookspec vs. call mis-matches Warn when either a hook call doesn't match a hookspec. Additionally, - Extend `varnames()` to return the list of both the arg and kwarg names for a function - Rename `_MultiCall.kwargs` to `caller_kwargs` to be more explicit - Store hookspec kwargs on `_HookRelay` and `HookImpl` instances Relates to #15 --- pluggy.py | 90 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/pluggy.py b/pluggy.py index e4444ff4..5ac2518b 100644 --- a/pluggy.py +++ b/pluggy.py @@ -1,5 +1,6 @@ import sys import inspect +import warnings __version__ = '0.5.0' @@ -9,6 +10,14 @@ _py3 = sys.version_info > (3, 0) +class PluginValidationError(Exception): + """ plugin failed validation. """ + + +class HookCallError(Exception): + """ Hook was called wrongly. """ + + class HookspecMarker(object): """ Decorator helper class for marking functions as hook specifications. @@ -266,7 +275,9 @@ def __init__(self, project_name, implprefix=None): self.hook = _HookRelay(self.trace.root.get("hook")) self._implprefix = implprefix self._inner_hookexec = lambda hook, methods, kwargs: \ - _MultiCall(methods, kwargs, hook.spec_opts).execute() + _MultiCall( + methods, kwargs, specopts=hook.spec_opts, hook=hook + ).execute() def _hookexec(self, hook, methods, kwargs): # called from all hookcaller instances. @@ -412,14 +423,16 @@ def _verify_hook(self, hook, hookimpl): "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" % (hookimpl.plugin_name, hook.name)) - for arg in hookimpl.argnames: - if arg not in hook.argnames: - raise PluginValidationError( - "Plugin %r\nhook %r\nargument %r not available\n" - "plugin definition: %s\n" - "available hookargs: %s" % - (hookimpl.plugin_name, hook.name, arg, - _formatdef(hookimpl.function), ", ".join(hook.argnames))) + # positional arg checking + notinspec = set(hookimpl.argnames) - set(hook.argnames) + if notinspec: + raise PluginValidationError( + "Plugin %r for hook %r\nhookimpl definition: %s\n" + "Positional args %s are declared in the hookimpl but " + "can not be found in the hookspec" % + (hookimpl.plugin_name, hook.name, + _formatdef(hookimpl.function), notinspec) + ) def check_pending(self): """ Verify that all hooks which have not been verified against @@ -526,24 +539,25 @@ class _MultiCall(object): # so we can remove it soon, allowing to avoid the below recursion # in execute() and simplify/speed up the execute loop. - def __init__(self, hook_impls, kwargs, specopts={}): + def __init__(self, hook_impls, kwargs, specopts={}, hook=None): + self.hook = hook self.hook_impls = hook_impls - self.kwargs = kwargs - self.kwargs["__multicall__"] = self - self.specopts = specopts + self.caller_kwargs = kwargs # come from _HookCaller.__call__() + self.caller_kwargs["__multicall__"] = self + self.specopts = hook.spec_opts if hook else specopts def execute(self): - all_kwargs = self.kwargs + caller_kwargs = self.caller_kwargs self.results = results = [] firstresult = self.specopts.get("firstresult") while self.hook_impls: hook_impl = self.hook_impls.pop() try: - args = [all_kwargs[argname] for argname in hook_impl.argnames] + args = [caller_kwargs[argname] for argname in hook_impl.argnames] except KeyError: for argname in hook_impl.argnames: - if argname not in all_kwargs: + if argname not in caller_kwargs: raise HookCallError( "hook call must provide argument %r" % (argname,)) if hook_impl.hookwrapper: @@ -561,7 +575,7 @@ def __repr__(self): status = "%d meths" % (len(self.hook_impls),) if hasattr(self, "results"): status = ("%d results, " % len(self.results)) + status - return "<_MultiCall %s, kwargs=%r>" % (status, self.kwargs) + return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs) def varnames(func): @@ -581,7 +595,7 @@ def varnames(func): try: func = func.__init__ except AttributeError: - return () + return (), () elif not inspect.isroutine(func): # callable object? try: func = getattr(func, '__call__', func) @@ -591,10 +605,14 @@ def varnames(func): try: # func MUST be a function or method here or we won't parse any args spec = inspect.getargspec(func) except TypeError: - return () + return (), () - args, defaults = spec.args, spec.defaults - args = args[:-len(defaults)] if defaults else args + args, defaults = tuple(spec.args), spec.defaults + if defaults: + index = -len(defaults) + args, defaults = args[:index], tuple(args[index:]) + else: + defaults = () # strip any implicit instance arg if args: @@ -605,10 +623,10 @@ def varnames(func): assert "self" not in args # best naming practises check? try: - cache["_varnames"] = args + cache["_varnames"] = args, defaults except TypeError: pass - return tuple(args) + return args, defaults class _HookRelay(object): @@ -627,6 +645,8 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None) self._wrappers = [] self._nonwrappers = [] self._hookexec = hook_execute + self.argnames = None + self.kwargnames = None if specmodule_or_class is not None: assert spec_opts is not None self.set_specification(specmodule_or_class, spec_opts) @@ -638,7 +658,8 @@ def set_specification(self, specmodule_or_class, spec_opts): assert not self.has_spec() self._specmodule_or_class = specmodule_or_class specfunc = getattr(specmodule_or_class, self.name) - argnames = varnames(specfunc) + # get spec arg signature + argnames, self.kwargnames = varnames(specfunc) self.argnames = ["__multicall__"] + list(argnames) self.spec_opts = spec_opts if spec_opts.get("historic"): @@ -658,6 +679,8 @@ def remove(wrappers): raise ValueError("plugin %r not found" % (plugin,)) def _add_hookimpl(self, hookimpl): + """A an implementation to the callback chain. + """ if hookimpl.hookwrapper: methods = self._wrappers else: @@ -679,6 +702,13 @@ def __repr__(self): def __call__(self, **kwargs): assert not self.is_historic() + notincall = set(self.argnames) - set(kwargs.keys()) + if notincall: + warnings.warn( + "Positional arg(s) %s are declared in the hookspec " + "but can not be found in this hook call" % notincall, + FutureWarning + ) return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) def call_historic(self, proc=None, kwargs=None): @@ -708,6 +738,8 @@ def call_extra(self, methods, kwargs): self._nonwrappers, self._wrappers = old def _maybe_apply_history(self, method): + """Apply call history to a new hookimpl if it is marked as historic. + """ if self.is_historic(): for kwargs, proc in self._call_history: res = self._hookexec(self, [method], kwargs) @@ -718,21 +750,13 @@ def _maybe_apply_history(self, method): class HookImpl(object): def __init__(self, plugin, plugin_name, function, hook_impl_opts): self.function = function - self.argnames = varnames(self.function) + self.argnames, self.kwargnames = varnames(self.function) self.plugin = plugin self.opts = hook_impl_opts self.plugin_name = plugin_name self.__dict__.update(hook_impl_opts) -class PluginValidationError(Exception): - """ plugin failed validation. """ - - -class HookCallError(Exception): - """ Hook was called wrongly. """ - - if hasattr(inspect, 'signature'): def _formatdef(func): return "%s%s" % (