Skip to content

Commit 0fc2930

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 0fc2930

File tree

3 files changed

+49
-23
lines changed

3 files changed

+49
-23
lines changed

pluggy.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,8 @@ def __init__(self, project_name, implprefix=None):
342342
self._implprefix = implprefix
343343
self._inner_hookexec = lambda hook, methods, kwargs: \
344344
_MultiCall(
345-
methods, kwargs, specopts=hook.spec_opts, hook=hook
346-
).execute()
345+
methods, kwargs, firstresult=hook.spec.opts['firstresult'],
346+
hook=hook).execute()
347347

348348
def _hookexec(self, hook, methods, kwargs):
349349
# called from all hookcaller instances.
@@ -490,7 +490,7 @@ def _verify_hook(self, hook, hookimpl):
490490
(hookimpl.plugin_name, hook.name))
491491

492492
# positional arg checking
493-
notinspec = set(hookimpl.argnames) - set(hook.argnames)
493+
notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
494494
if notinspec:
495495
raise PluginValidationError(
496496
"Plugin %r for hook %r\nhookimpl definition: %s\n"
@@ -583,8 +583,8 @@ def subset_hook_caller(self, name, remove_plugins):
583583
orig = getattr(self.hook, name)
584584
plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
585585
if plugins_to_remove:
586-
hc = _HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class,
587-
orig.spec_opts)
586+
hc = _HookCaller(orig.name, orig._hookexec, orig.spec.namespace,
587+
orig.spec.opts)
588588
for hookimpl in (orig._wrappers + orig._nonwrappers):
589589
plugin = hookimpl.plugin
590590
if plugin not in plugins_to_remove:
@@ -605,29 +605,40 @@ class _MultiCall:
605605
# so we can remove it soon, allowing to avoid the below recursion
606606
# in execute() and simplify/speed up the execute loop.
607607

608-
def __init__(self, hook_impls, kwargs, specopts={}, hook=None):
608+
def __init__(self, hook_impls, kwargs, firstresult=False, hook=None):
609609
self.hook = hook
610610
self.hook_impls = hook_impls
611611
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
612612
self.caller_kwargs["__multicall__"] = self
613-
self.specopts = hook.spec_opts if hook else specopts
613+
self.firstresult = firstresult
614614

615615
def execute(self):
616616
caller_kwargs = self.caller_kwargs
617617
self.results = results = []
618-
firstresult = self.specopts.get("firstresult")
618+
firstresult = self.firstresult
619619

620620
while self.hook_impls:
621621
hook_impl = self.hook_impls.pop()
622622
try:
623623
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
624+
# get any caller provided kwargs declared in our
625+
# hookimpl and fail over to the spec's value if provided
626+
if self.hook and hook_impl.kwargnames:
627+
args += [
628+
caller_kwargs.get(
629+
argname, self.hook.spec.kwargs.get(
630+
argname, hook_impl.kwargs[argname]))
631+
for argname in hook_impl.kwargnames
632+
]
624633
except KeyError:
625634
for argname in hook_impl.argnames:
626635
if argname not in caller_kwargs:
627636
raise HookCallError(
628637
"hook call must provide argument %r" % (argname,))
638+
629639
if hook_impl.hookwrapper:
630640
return _wrapped_call(hook_impl.function(*args), self.execute)
641+
631642
res = hook_impl.function(*args)
632643
if res is not None:
633644
if firstresult:
@@ -711,28 +722,23 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None)
711722
self._wrappers = []
712723
self._nonwrappers = []
713724
self._hookexec = hook_execute
714-
self.argnames = None
715-
self.kwargnames = None
725+
self.spec = None
726+
self._call_history = None
716727
if specmodule_or_class is not None:
717728
assert spec_opts is not None
718729
self.set_specification(specmodule_or_class, spec_opts)
719730

720731
def has_spec(self):
721-
return hasattr(self, "_specmodule_or_class")
732+
return self.spec is not None
722733

723734
def set_specification(self, specmodule_or_class, spec_opts):
724735
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
736+
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
731737
if spec_opts.get("historic"):
732738
self._call_history = []
733739

734740
def is_historic(self):
735-
return hasattr(self, "_call_history")
741+
return self._call_history is not None
736742

737743
def _remove_plugin(self, plugin):
738744
def remove(wrappers):
@@ -768,7 +774,7 @@ def __repr__(self):
768774

769775
def __call__(self, **kwargs):
770776
assert not self.is_historic()
771-
notincall = set(self.argnames) - set(kwargs.keys())
777+
notincall = set(self.spec.argnames) - set(kwargs.keys())
772778
if notincall:
773779
warnings.warn(
774780
"Positional arg(s) %s are declared in the hookspec "
@@ -813,10 +819,30 @@ def _maybe_apply_history(self, method):
813819
proc(res[0])
814820

815821

822+
class HookSpec:
823+
def __init__(self, namespace, name, hook_spec_opts):
824+
self.namespace = namespace
825+
self.function = function = getattr(namespace, name)
826+
self.name = name
827+
self.argnames, self.kwargnames = varnames(function)
828+
self.kwargvalues = inspect.getargspec(function).defaults
829+
self.kwargs = dict(
830+
((name, value) for name, value in
831+
zip(self.kwargnames, inspect.getargspec(function).defaults))
832+
) if self.kwargvalues else {}
833+
self.opts = hook_spec_opts
834+
self.argnames = ["__multicall__"] + list(self.argnames)
835+
836+
816837
class HookImpl:
817838
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
818839
self.function = function
819840
self.argnames, self.kwargnames = varnames(self.function)
841+
self.kwargvalues = inspect.getargspec(function).defaults
842+
self.kwargs = dict(
843+
((name, value) for name, value in
844+
zip(self.kwargnames, inspect.getargspec(function).defaults))
845+
) if self.kwargvalues else {}
820846
self.plugin = plugin
821847
self.opts = hook_impl_opts
822848
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)