Skip to content

Commit 1b4aceb

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 21771da commit 1b4aceb

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
@@ -1,5 +1,6 @@
11
import sys
22
import inspect
3+
import warnings
34

45
__version__ = '0.5.0'
56

@@ -9,6 +10,14 @@
910
_py3 = sys.version_info > (3, 0)
1011

1112

13+
class PluginValidationError(Exception):
14+
""" plugin failed validation. """
15+
16+
17+
class HookCallError(Exception):
18+
""" Hook was called wrongly. """
19+
20+
1221
class HookspecMarker:
1322
""" Decorator helper class for marking functions as hook specifications.
1423
@@ -266,7 +275,9 @@ def __init__(self, project_name, implprefix=None):
266275
self.hook = _HookRelay(self.trace.root.get("hook"))
267276
self._implprefix = implprefix
268277
self._inner_hookexec = lambda hook, methods, kwargs: \
269-
_MultiCall(methods, kwargs, hook.spec_opts).execute()
278+
_MultiCall(
279+
methods, kwargs, specopts=hook.spec_opts, hook=hook
280+
).execute()
270281

271282
def _hookexec(self, hook, methods, kwargs):
272283
# called from all hookcaller instances.
@@ -412,14 +423,16 @@ def _verify_hook(self, hook, hookimpl):
412423
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %
413424
(hookimpl.plugin_name, hook.name))
414425

415-
for arg in hookimpl.argnames:
416-
if arg not in hook.argnames:
417-
raise PluginValidationError(
418-
"Plugin %r\nhook %r\nargument %r not available\n"
419-
"plugin definition: %s\n"
420-
"available hookargs: %s" %
421-
(hookimpl.plugin_name, hook.name, arg,
422-
_formatdef(hookimpl.function), ", ".join(hook.argnames)))
426+
# positional arg checking
427+
notinspec = set(hookimpl.argnames) - set(hook.argnames)
428+
if notinspec:
429+
raise PluginValidationError(
430+
"Plugin %r for hook %r\nhookimpl definition: %s\n"
431+
"Positional args %s are declared in the hookimpl but "
432+
"can not be found in the hookspec" %
433+
(hookimpl.plugin_name, hook.name,
434+
_formatdef(hookimpl.function), notinspec)
435+
)
423436

424437
def check_pending(self):
425438
""" Verify that all hooks which have not been verified against
@@ -526,24 +539,25 @@ class _MultiCall:
526539
# so we can remove it soon, allowing to avoid the below recursion
527540
# in execute() and simplify/speed up the execute loop.
528541

529-
def __init__(self, hook_impls, kwargs, specopts={}):
542+
def __init__(self, hook_impls, kwargs, specopts={}, hook=None):
543+
self.hook = hook
530544
self.hook_impls = hook_impls
531-
self.kwargs = kwargs
532-
self.kwargs["__multicall__"] = self
533-
self.specopts = specopts
545+
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
546+
self.caller_kwargs["__multicall__"] = self
547+
self.specopts = hook.spec_opts if hook else specopts
534548

535549
def execute(self):
536-
all_kwargs = self.kwargs
550+
caller_kwargs = self.caller_kwargs
537551
self.results = results = []
538552
firstresult = self.specopts.get("firstresult")
539553

540554
while self.hook_impls:
541555
hook_impl = self.hook_impls.pop()
542556
try:
543-
args = [all_kwargs[argname] for argname in hook_impl.argnames]
557+
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
544558
except KeyError:
545559
for argname in hook_impl.argnames:
546-
if argname not in all_kwargs:
560+
if argname not in caller_kwargs:
547561
raise HookCallError(
548562
"hook call must provide argument %r" % (argname,))
549563
if hook_impl.hookwrapper:
@@ -561,7 +575,7 @@ def __repr__(self):
561575
status = "%d meths" % (len(self.hook_impls),)
562576
if hasattr(self, "results"):
563577
status = ("%d results, " % len(self.results)) + status
564-
return "<_MultiCall %s, kwargs=%r>" % (status, self.kwargs)
578+
return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs)
565579

566580

567581
def varnames(func):
@@ -581,7 +595,7 @@ def varnames(func):
581595
try:
582596
func = func.__init__
583597
except AttributeError:
584-
return ()
598+
return (), ()
585599
elif not inspect.isroutine(func): # callable object?
586600
try:
587601
func = getattr(func, '__call__', func)
@@ -591,10 +605,14 @@ def varnames(func):
591605
try: # func MUST be a function or method here or we won't parse any args
592606
spec = inspect.getargspec(func)
593607
except TypeError:
594-
return ()
608+
return (), ()
595609

596-
args, defaults = spec.args, spec.defaults
597-
args = args[:-len(defaults)] if defaults else args
610+
args, defaults = tuple(spec.args), spec.defaults
611+
if defaults:
612+
index = -len(defaults)
613+
args, defaults = args[:index], tuple(args[index:])
614+
else:
615+
defaults = ()
598616

599617
# strip any implicit instance arg
600618
if args:
@@ -605,10 +623,10 @@ def varnames(func):
605623

606624
assert "self" not in args # best naming practises check?
607625
try:
608-
cache["_varnames"] = args
626+
cache["_varnames"] = args, defaults
609627
except TypeError:
610628
pass
611-
return tuple(args)
629+
return args, defaults
612630

613631

614632
class _HookRelay:
@@ -627,6 +645,8 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None)
627645
self._wrappers = []
628646
self._nonwrappers = []
629647
self._hookexec = hook_execute
648+
self.argnames = None
649+
self.kwargnames = None
630650
if specmodule_or_class is not None:
631651
assert spec_opts is not None
632652
self.set_specification(specmodule_or_class, spec_opts)
@@ -638,7 +658,8 @@ def set_specification(self, specmodule_or_class, spec_opts):
638658
assert not self.has_spec()
639659
self._specmodule_or_class = specmodule_or_class
640660
specfunc = getattr(specmodule_or_class, self.name)
641-
argnames = varnames(specfunc)
661+
# get spec arg signature
662+
argnames, self.kwargnames = varnames(specfunc)
642663
self.argnames = ["__multicall__"] + list(argnames)
643664
self.spec_opts = spec_opts
644665
if spec_opts.get("historic"):
@@ -658,6 +679,8 @@ def remove(wrappers):
658679
raise ValueError("plugin %r not found" % (plugin,))
659680

660681
def _add_hookimpl(self, hookimpl):
682+
"""A an implementation to the callback chain.
683+
"""
661684
if hookimpl.hookwrapper:
662685
methods = self._wrappers
663686
else:
@@ -679,6 +702,13 @@ def __repr__(self):
679702

680703
def __call__(self, **kwargs):
681704
assert not self.is_historic()
705+
notincall = set(self.argnames) - set(kwargs.keys())
706+
if notincall:
707+
warnings.warn(
708+
"Positional arg(s) %s are declared in the hookspec "
709+
"but can not be found in this hook call" % notincall,
710+
FutureWarning
711+
)
682712
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
683713

684714
def call_historic(self, proc=None, kwargs=None):
@@ -708,6 +738,8 @@ def call_extra(self, methods, kwargs):
708738
self._nonwrappers, self._wrappers = old
709739

710740
def _maybe_apply_history(self, method):
741+
"""Apply call history to a new hookimpl if it is marked as historic.
742+
"""
711743
if self.is_historic():
712744
for kwargs, proc in self._call_history:
713745
res = self._hookexec(self, [method], kwargs)
@@ -718,21 +750,13 @@ def _maybe_apply_history(self, method):
718750
class HookImpl:
719751
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
720752
self.function = function
721-
self.argnames = varnames(self.function)
753+
self.argnames, self.kwargnames = varnames(self.function)
722754
self.plugin = plugin
723755
self.opts = hook_impl_opts
724756
self.plugin_name = plugin_name
725757
self.__dict__.update(hook_impl_opts)
726758

727759

728-
class PluginValidationError(Exception):
729-
""" plugin failed validation. """
730-
731-
732-
class HookCallError(Exception):
733-
""" Hook was called wrongly. """
734-
735-
736760
if hasattr(inspect, 'signature'):
737761
def _formatdef(func):
738762
return "%s%s" % (

0 commit comments

Comments
 (0)