Skip to content

Commit f849553

Browse files
author
Tyler Goodlet
committed
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
1 parent 5182582 commit f849553

File tree

1 file changed

+57
-33
lines changed

1 file changed

+57
-33
lines changed

pluggy.py

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"""
6767
import sys
6868
import inspect
69+
import warnings
6970

7071
__version__ = '0.5.0'
7172

@@ -75,6 +76,14 @@
7576
_py3 = sys.version_info > (3, 0)
7677

7778

79+
class PluginValidationError(Exception):
80+
""" plugin failed validation. """
81+
82+
83+
class HookCallError(Exception):
84+
""" Hook was called wrongly. """
85+
86+
7887
class HookspecMarker:
7988
""" Decorator helper class for marking functions as hook specifications.
8089
@@ -332,7 +341,9 @@ def __init__(self, project_name, implprefix=None):
332341
self.hook = _HookRelay(self.trace.root.get("hook"))
333342
self._implprefix = implprefix
334343
self._inner_hookexec = lambda hook, methods, kwargs: \
335-
_MultiCall(methods, kwargs, hook.spec_opts).execute()
344+
_MultiCall(
345+
methods, kwargs, specopts=hook.spec_opts, hook=hook
346+
).execute()
336347

337348
def _hookexec(self, hook, methods, kwargs):
338349
# called from all hookcaller instances.
@@ -478,14 +489,16 @@ def _verify_hook(self, hook, hookimpl):
478489
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %
479490
(hookimpl.plugin_name, hook.name))
480491

481-
for arg in hookimpl.argnames:
482-
if arg not in hook.argnames:
483-
raise PluginValidationError(
484-
"Plugin %r\nhook %r\nargument %r not available\n"
485-
"plugin definition: %s\n"
486-
"available hookargs: %s" %
487-
(hookimpl.plugin_name, hook.name, arg,
488-
_formatdef(hookimpl.function), ", ".join(hook.argnames)))
492+
# positional arg checking
493+
notinspec = set(hookimpl.argnames) - set(hook.argnames)
494+
if notinspec:
495+
raise PluginValidationError(
496+
"Plugin %r for hook %r\nhookimpl definition: %s\n"
497+
"Positional args %s are declared in the hookimpl but "
498+
"can not be found in the hookspec" %
499+
(hookimpl.plugin_name, hook.name,
500+
_formatdef(hookimpl.function), notinspec)
501+
)
489502

490503
def check_pending(self):
491504
""" Verify that all hooks which have not been verified against
@@ -592,24 +605,25 @@ class _MultiCall:
592605
# so we can remove it soon, allowing to avoid the below recursion
593606
# in execute() and simplify/speed up the execute loop.
594607

595-
def __init__(self, hook_impls, kwargs, specopts={}):
608+
def __init__(self, hook_impls, kwargs, specopts={}, hook=None):
609+
self.hook = hook
596610
self.hook_impls = hook_impls
597-
self.kwargs = kwargs
598-
self.kwargs["__multicall__"] = self
599-
self.specopts = specopts
611+
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
612+
self.caller_kwargs["__multicall__"] = self
613+
self.specopts = hook.spec_opts if hook else specopts
600614

601615
def execute(self):
602-
all_kwargs = self.kwargs
616+
caller_kwargs = self.caller_kwargs
603617
self.results = results = []
604618
firstresult = self.specopts.get("firstresult")
605619

606620
while self.hook_impls:
607621
hook_impl = self.hook_impls.pop()
608622
try:
609-
args = [all_kwargs[argname] for argname in hook_impl.argnames]
623+
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
610624
except KeyError:
611625
for argname in hook_impl.argnames:
612-
if argname not in all_kwargs:
626+
if argname not in caller_kwargs:
613627
raise HookCallError(
614628
"hook call must provide argument %r" % (argname,))
615629
if hook_impl.hookwrapper:
@@ -627,7 +641,7 @@ def __repr__(self):
627641
status = "%d meths" % (len(self.hook_impls),)
628642
if hasattr(self, "results"):
629643
status = ("%d results, " % len(self.results)) + status
630-
return "<_MultiCall %s, kwargs=%r>" % (status, self.kwargs)
644+
return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs)
631645

632646

633647
def varnames(func):
@@ -647,7 +661,7 @@ def varnames(func):
647661
try:
648662
func = func.__init__
649663
except AttributeError:
650-
return ()
664+
return (), ()
651665
elif not inspect.isroutine(func): # callable object?
652666
try:
653667
func = getattr(func, '__call__', func)
@@ -657,10 +671,14 @@ def varnames(func):
657671
try: # func MUST be a function or method here or we won't parse any args
658672
spec = inspect.getargspec(func)
659673
except TypeError:
660-
return ()
674+
return (), ()
661675

662-
args, defaults = spec.args, spec.defaults
663-
args = args[:-len(defaults)] if defaults else args
676+
args, defaults = tuple(spec.args), spec.defaults
677+
if defaults:
678+
index = -len(defaults)
679+
args, defaults = args[:index], tuple(args[index:])
680+
else:
681+
defaults = ()
664682

665683
# strip any implicit instance arg
666684
if args:
@@ -671,10 +689,10 @@ def varnames(func):
671689

672690
assert "self" not in args # best naming practises check?
673691
try:
674-
cache["_varnames"] = args
692+
cache["_varnames"] = args, defaults
675693
except TypeError:
676694
pass
677-
return tuple(args)
695+
return args, defaults
678696

679697

680698
class _HookRelay:
@@ -693,6 +711,8 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None)
693711
self._wrappers = []
694712
self._nonwrappers = []
695713
self._hookexec = hook_execute
714+
self.argnames = None
715+
self.kwargnames = None
696716
if specmodule_or_class is not None:
697717
assert spec_opts is not None
698718
self.set_specification(specmodule_or_class, spec_opts)
@@ -704,7 +724,8 @@ def set_specification(self, specmodule_or_class, spec_opts):
704724
assert not self.has_spec()
705725
self._specmodule_or_class = specmodule_or_class
706726
specfunc = getattr(specmodule_or_class, self.name)
707-
argnames = varnames(specfunc)
727+
# get spec arg signature
728+
argnames, self.kwargnames = varnames(specfunc)
708729
self.argnames = ["__multicall__"] + list(argnames)
709730
self.spec_opts = spec_opts
710731
if spec_opts.get("historic"):
@@ -724,6 +745,8 @@ def remove(wrappers):
724745
raise ValueError("plugin %r not found" % (plugin,))
725746

726747
def _add_hookimpl(self, hookimpl):
748+
"""A an implementation to the callback chain.
749+
"""
727750
if hookimpl.hookwrapper:
728751
methods = self._wrappers
729752
else:
@@ -745,6 +768,13 @@ def __repr__(self):
745768

746769
def __call__(self, **kwargs):
747770
assert not self.is_historic()
771+
notincall = set(self.argnames) - set(kwargs.keys())
772+
if notincall:
773+
warnings.warn(
774+
"Positional arg(s) %s are declared in the hookspec "
775+
"but can not be found in this hook call" % notincall,
776+
FutureWarning
777+
)
748778
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
749779

750780
def call_historic(self, proc=None, kwargs=None):
@@ -774,6 +804,8 @@ def call_extra(self, methods, kwargs):
774804
self._nonwrappers, self._wrappers = old
775805

776806
def _maybe_apply_history(self, method):
807+
"""Apply call history to a new hookimpl if it is marked as historic.
808+
"""
777809
if self.is_historic():
778810
for kwargs, proc in self._call_history:
779811
res = self._hookexec(self, [method], kwargs)
@@ -784,21 +816,13 @@ def _maybe_apply_history(self, method):
784816
class HookImpl:
785817
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
786818
self.function = function
787-
self.argnames = varnames(self.function)
819+
self.argnames, self.kwargnames = varnames(self.function)
788820
self.plugin = plugin
789821
self.opts = hook_impl_opts
790822
self.plugin_name = plugin_name
791823
self.__dict__.update(hook_impl_opts)
792824

793825

794-
class PluginValidationError(Exception):
795-
""" plugin failed validation. """
796-
797-
798-
class HookCallError(Exception):
799-
""" Hook was called wrongly. """
800-
801-
802826
if hasattr(inspect, 'signature'):
803827
def _formatdef(func):
804828
return "%s%s" % (

0 commit comments

Comments
 (0)