Skip to content

Commit d743939

Browse files
author
Tyler Goodlet
committed
Add defaults support to enable deprecation
Add support for using declared keyword argument values from both hookspecs and hookimpls. The current logic will inspect the hookimpl and, if it contains defaults, values will be first looked up from the caller provided data and if not defined will be taken from the hookspec's declared defaults. If the spec does not define defaults the value is taken from the hookimpl's defaults as is expected under normal function call semantics. Resolves #15
1 parent 22f3b0a commit d743939

File tree

3 files changed

+65
-32
lines changed

3 files changed

+65
-32
lines changed

pluggy.py

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

7172
__version__ = '0.5.0'
@@ -340,10 +341,12 @@ def __init__(self, project_name, implprefix=None):
340341
self.trace = _TagTracer().get("pluginmanage")
341342
self.hook = _HookRelay(self.trace.root.get("hook"))
342343
self._implprefix = implprefix
343-
self._inner_hookexec = lambda hook, methods, kwargs: \
344-
_MultiCall(
345-
methods, kwargs, specopts=hook.spec_opts, hook=hook
346-
).execute()
344+
self._inner_hookexec = lambda hook, methods, kwargs: _MultiCall(
345+
methods,
346+
kwargs,
347+
firstresult=hook.spec.opts['firstresult'] if hook.spec else False,
348+
hook=hook
349+
).execute()
347350

348351
def _hookexec(self, hook, methods, kwargs):
349352
# called from all hookcaller instances.
@@ -490,7 +493,7 @@ def _verify_hook(self, hook, hookimpl):
490493
(hookimpl.plugin_name, hook.name))
491494

492495
# positional arg checking
493-
notinspec = set(hookimpl.argnames) - set(hook.argnames)
496+
notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
494497
if notinspec:
495498
raise PluginValidationError(
496499
"Plugin %r for hook %r\nhookimpl definition: %s\n"
@@ -583,8 +586,8 @@ def subset_hook_caller(self, name, remove_plugins):
583586
orig = getattr(self.hook, name)
584587
plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
585588
if plugins_to_remove:
586-
hc = _HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class,
587-
orig.spec_opts)
589+
hc = _HookCaller(orig.name, orig._hookexec, orig.spec.namespace,
590+
orig.spec.opts)
588591
for hookimpl in (orig._wrappers + orig._nonwrappers):
589592
plugin = hookimpl.plugin
590593
if plugin not in plugins_to_remove:
@@ -605,29 +608,43 @@ class _MultiCall:
605608
# so we can remove it soon, allowing to avoid the below recursion
606609
# in execute() and simplify/speed up the execute loop.
607610

608-
def __init__(self, hook_impls, kwargs, specopts={}, hook=None):
609-
self.hook = hook
611+
def __init__(self, hook_impls, kwargs, firstresult=False, hook=None):
610612
self.hook_impls = hook_impls
611613
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
612614
self.caller_kwargs["__multicall__"] = self
613-
self.specopts = hook.spec_opts if hook else specopts
615+
self.firstresult = firstresult
616+
self.hook = hook
617+
self.spec = hook.spec if hook else None
614618

615619
def execute(self):
616620
caller_kwargs = self.caller_kwargs
617621
self.results = results = []
618-
firstresult = self.specopts.get("firstresult")
622+
firstresult = self.firstresult
623+
spec = self.spec
619624

620625
while self.hook_impls:
621626
hook_impl = self.hook_impls.pop()
627+
implkwargs = hook_impl.kwargs
622628
try:
623629
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
630+
# get any caller provided kwargs declared in our
631+
# hookimpl and fail over to the spec's value if provided
632+
if implkwargs:
633+
kwargs = copy.copy(implkwargs)
634+
if spec:
635+
kwargs.update(spec.kwargs)
636+
637+
args += [caller_kwargs.get(argname, kwargs[argname])
638+
for argname in hook_impl.kwargnames]
624639
except KeyError:
625640
for argname in hook_impl.argnames:
626641
if argname not in caller_kwargs:
627642
raise HookCallError(
628643
"hook call must provide argument %r" % (argname,))
644+
629645
if hook_impl.hookwrapper:
630646
return _wrapped_call(hook_impl.function(*args), self.execute)
647+
631648
res = hook_impl.function(*args)
632649
if res is not None:
633650
if firstresult:
@@ -711,28 +728,23 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None)
711728
self._wrappers = []
712729
self._nonwrappers = []
713730
self._hookexec = hook_execute
714-
self.argnames = None
715-
self.kwargnames = None
731+
self.spec = None
732+
self._call_history = None
716733
if specmodule_or_class is not None:
717734
assert spec_opts is not None
718735
self.set_specification(specmodule_or_class, spec_opts)
719736

720737
def has_spec(self):
721-
return hasattr(self, "_specmodule_or_class")
738+
return self.spec is not None
722739

723740
def set_specification(self, specmodule_or_class, spec_opts):
724741
assert not self.has_spec()
725-
self._specmodule_or_class = specmodule_or_class
726-
specfunc = getattr(specmodule_or_class, self.name)
727-
# get spec arg signature
728-
argnames, self.kwargnames = varnames(specfunc)
729-
self.argnames = ["__multicall__"] + list(argnames)
730-
self.spec_opts = spec_opts
742+
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
731743
if spec_opts.get("historic"):
732744
self._call_history = []
733745

734746
def is_historic(self):
735-
return hasattr(self, "_call_history")
747+
return self._call_history is not None
736748

737749
def _remove_plugin(self, plugin):
738750
def remove(wrappers):
@@ -768,13 +780,14 @@ def __repr__(self):
768780

769781
def __call__(self, **kwargs):
770782
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-
)
783+
if self.spec:
784+
notincall = set(self.spec.argnames) - set(kwargs.keys())
785+
if notincall:
786+
warnings.warn(
787+
"Positional arg(s) %s are declared in the hookspec "
788+
"but can not be found in this hook call" % notincall,
789+
FutureWarning
790+
)
778791
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
779792

780793
def call_historic(self, proc=None, kwargs=None):
@@ -813,10 +826,30 @@ def _maybe_apply_history(self, method):
813826
proc(res[0])
814827

815828

829+
class HookSpec:
830+
def __init__(self, namespace, name, hook_spec_opts):
831+
self.namespace = namespace
832+
self.function = function = getattr(namespace, name)
833+
self.name = name
834+
self.argnames, self.kwargnames = varnames(function)
835+
self.kwargvalues = inspect.getargspec(function).defaults
836+
self.kwargs = dict(
837+
((name, value) for name, value in
838+
zip(self.kwargnames, inspect.getargspec(function).defaults))
839+
) if self.kwargvalues else {}
840+
self.opts = hook_spec_opts
841+
self.argnames = ["__multicall__"] + list(self.argnames)
842+
843+
816844
class HookImpl:
817845
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
818846
self.function = function
819847
self.argnames, self.kwargnames = varnames(self.function)
848+
self.kwargvalues = inspect.getargspec(function).defaults
849+
self.kwargs = dict(
850+
((name, value) for name, value in
851+
zip(self.kwargnames, inspect.getargspec(function).defaults))
852+
) if self.kwargvalues else {}
820853
self.plugin = plugin
821854
self.opts = hook_impl_opts
822855
self.plugin_name = plugin_name

testing/test_method_ordering.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,9 @@ def he_myhook3(arg1):
163163
pass
164164

165165
pm.add_hookspecs(HookSpec)
166-
assert not pm.hook.he_myhook1.spec_opts["firstresult"]
167-
assert pm.hook.he_myhook2.spec_opts["firstresult"]
168-
assert not pm.hook.he_myhook3.spec_opts["firstresult"]
166+
assert not pm.hook.he_myhook1.spec.opts["firstresult"]
167+
assert pm.hook.he_myhook2.spec.opts["firstresult"]
168+
assert not pm.hook.he_myhook3.spec.opts["firstresult"]
169169

170170

171171
@pytest.mark.parametrize('name', ["hookwrapper", "optionalhook", "tryfirst", "trylast"])

testing/test_multicall.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def MC(methods, kwargs, firstresult=False):
2222
for method in methods:
2323
f = HookImpl(None, "<temp>", method, method.example_impl)
2424
hookfuncs.append(f)
25-
return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult})
25+
return _MultiCall(hookfuncs, kwargs, firstresult=firstresult)
2626

2727

2828
def test_call_passing():

0 commit comments

Comments
 (0)