Skip to content

Commit dd4641e

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 9ab0ebe commit dd4641e

File tree

2 files changed

+49
-23
lines changed

2 files changed

+49
-23
lines changed

pluggy.py

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

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

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

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

614614
def execute(self):
615615
caller_kwargs = self.caller_kwargs
616616
self.results = results = []
617-
firstresult = self.specopts.get("firstresult")
617+
firstresult = self.firstresult
618618

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

719730
def has_spec(self):
720-
return hasattr(self, "_specmodule_or_class")
731+
return self.spec is not None
721732

722733
def set_specification(self, specmodule_or_class, spec_opts):
723734
assert not self.has_spec()
724-
self._specmodule_or_class = specmodule_or_class
725-
specfunc = getattr(specmodule_or_class, self.name)
726-
# get spec arg signature
727-
argnames, self.kwargnames = varnames(specfunc)
728-
self.argnames = ["__multicall__"] + list(argnames)
729-
self.spec_opts = spec_opts
735+
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
730736
if spec_opts.get("historic"):
731737
self._call_history = []
732738

733739
def is_historic(self):
734-
return hasattr(self, "_call_history")
740+
return self._call_history is not None
735741

736742
def _remove_plugin(self, plugin):
737743
def remove(wrappers):
@@ -767,7 +773,7 @@ def __repr__(self):
767773

768774
def __call__(self, **kwargs):
769775
assert not self.is_historic()
770-
notincall = set(self.argnames) - set(kwargs.keys())
776+
notincall = set(self.spec.argnames) - set(kwargs.keys())
771777
if notincall:
772778
warnings.warn(
773779
"Positional arg(s) %s are declared in the hookspec "
@@ -812,10 +818,30 @@ def _maybe_apply_history(self, method):
812818
proc(res[0])
813819

814820

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

testing/test_pluggy.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -500,9 +500,9 @@ def he_myhook3(self, arg1):
500500
pass
501501

502502
pm.add_hookspecs(HookSpec)
503-
assert not pm.hook.he_myhook1.spec_opts["firstresult"]
504-
assert pm.hook.he_myhook2.spec_opts["firstresult"]
505-
assert not pm.hook.he_myhook3.spec_opts["firstresult"]
503+
assert not pm.hook.he_myhook1.spec.opts['firstresult']
504+
assert pm.hook.he_myhook2.spec.opts['firstresult']
505+
assert not pm.hook.he_myhook3.spec.opts['firstresult']
506506

507507
def test_hookimpl(self):
508508
for name in ["hookwrapper", "optionalhook", "tryfirst", "trylast"]:
@@ -805,7 +805,7 @@ def MC(self, methods, kwargs, firstresult=False):
805805
for method in methods:
806806
f = HookImpl(None, "<temp>", method, method.example_impl)
807807
hookfuncs.append(f)
808-
return _MultiCall(hookfuncs, kwargs, {"firstresult": firstresult})
808+
return _MultiCall(hookfuncs, kwargs, firstresult=firstresult)
809809

810810
def test_call_passing(self):
811811
class P1:

0 commit comments

Comments
 (0)