Skip to content

Commit ece9146

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 e5cfb31 commit ece9146

File tree

3 files changed

+57
-27
lines changed

3 files changed

+57
-27
lines changed

pluggy.py

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
import inspect
3+
import copy
34
import warnings
45

56
__version__ = '0.5.0'
@@ -274,10 +275,12 @@ def __init__(self, project_name, implprefix=None):
274275
self.trace = _TagTracer().get("pluginmanage")
275276
self.hook = _HookRelay(self.trace.root.get("hook"))
276277
self._implprefix = implprefix
277-
self._inner_hookexec = lambda hook, methods, kwargs: \
278-
_MultiCall(
279-
methods, kwargs, specopts=hook.spec_opts, hook=hook
280-
).execute()
278+
self._inner_hookexec = lambda hook, methods, kwargs: _MultiCall(
279+
methods,
280+
kwargs,
281+
firstresult=hook.spec.opts['firstresult'] if hook.spec else False,
282+
hook=hook
283+
).execute()
281284

282285
def _hookexec(self, hook, methods, kwargs):
283286
# called from all hookcaller instances.
@@ -424,7 +427,7 @@ def _verify_hook(self, hook, hookimpl):
424427
(hookimpl.plugin_name, hook.name))
425428

426429
# positional arg checking
427-
notinspec = set(hookimpl.argnames) - set(hook.argnames)
430+
notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
428431
if notinspec:
429432
raise PluginValidationError(
430433
"Plugin %r for hook %r\nhookimpl definition: %s\n"
@@ -517,8 +520,8 @@ def subset_hook_caller(self, name, remove_plugins):
517520
orig = getattr(self.hook, name)
518521
plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
519522
if plugins_to_remove:
520-
hc = _HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class,
521-
orig.spec_opts)
523+
hc = _HookCaller(orig.name, orig._hookexec, orig.spec.namespace,
524+
orig.spec.opts)
522525
for hookimpl in (orig._wrappers + orig._nonwrappers):
523526
plugin = hookimpl.plugin
524527
if plugin not in plugins_to_remove:
@@ -539,29 +542,43 @@ class _MultiCall(object):
539542
# so we can remove it soon, allowing to avoid the below recursion
540543
# in execute() and simplify/speed up the execute loop.
541544

542-
def __init__(self, hook_impls, kwargs, specopts={}, hook=None):
543-
self.hook = hook
545+
def __init__(self, hook_impls, kwargs, firstresult=False, hook=None):
544546
self.hook_impls = hook_impls
545547
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
546548
self.caller_kwargs["__multicall__"] = self
547-
self.specopts = hook.spec_opts if hook else specopts
549+
self.firstresult = firstresult
550+
self.hook = hook
551+
self.spec = hook.spec if hook else None
548552

549553
def execute(self):
550554
caller_kwargs = self.caller_kwargs
551555
self.results = results = []
552-
firstresult = self.specopts.get("firstresult")
556+
firstresult = self.firstresult
557+
spec = self.spec
553558

554559
while self.hook_impls:
555560
hook_impl = self.hook_impls.pop()
561+
implkwargs = hook_impl.kwargs
556562
try:
557563
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
564+
# get any caller provided kwargs declared in our
565+
# hookimpl and fail over to the spec's value if provided
566+
if implkwargs:
567+
kwargs = copy.copy(implkwargs)
568+
if spec:
569+
kwargs.update(spec.kwargs)
570+
571+
args += [caller_kwargs.get(argname, kwargs[argname])
572+
for argname in hook_impl.kwargnames]
558573
except KeyError:
559574
for argname in hook_impl.argnames:
560575
if argname not in caller_kwargs:
561576
raise HookCallError(
562577
"hook call must provide argument %r" % (argname,))
578+
563579
if hook_impl.hookwrapper:
564580
return _wrapped_call(hook_impl.function(*args), self.execute)
581+
565582
res = hook_impl.function(*args)
566583
if res is not None:
567584
if firstresult:
@@ -645,28 +662,23 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None)
645662
self._wrappers = []
646663
self._nonwrappers = []
647664
self._hookexec = hook_execute
648-
self.argnames = None
649-
self.kwargnames = None
665+
self.spec = None
666+
self._call_history = None
650667
if specmodule_or_class is not None:
651668
assert spec_opts is not None
652669
self.set_specification(specmodule_or_class, spec_opts)
653670

654671
def has_spec(self):
655-
return hasattr(self, "_specmodule_or_class")
672+
return self.spec is not None
656673

657674
def set_specification(self, specmodule_or_class, spec_opts):
658675
assert not self.has_spec()
659-
self._specmodule_or_class = specmodule_or_class
660-
specfunc = getattr(specmodule_or_class, self.name)
661-
# get spec arg signature
662-
argnames, self.kwargnames = varnames(specfunc)
663-
self.argnames = ["__multicall__"] + list(argnames)
664-
self.spec_opts = spec_opts
676+
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
665677
if spec_opts.get("historic"):
666678
self._call_history = []
667679

668680
def is_historic(self):
669-
return hasattr(self, "_call_history")
681+
return self._call_history is not None
670682

671683
def _remove_plugin(self, plugin):
672684
def remove(wrappers):
@@ -702,8 +714,8 @@ def __repr__(self):
702714

703715
def __call__(self, **kwargs):
704716
assert not self.is_historic()
705-
if self.argnames:
706-
notincall = set(self.argnames) - set(['__multicall__']) - set(
717+
if self.spec:
718+
notincall = set(self.spec.argnames) - set(['__multicall__']) - set(
707719
kwargs.keys())
708720
if notincall:
709721
warnings.warn(
@@ -749,10 +761,28 @@ def _maybe_apply_history(self, method):
749761
proc(res[0])
750762

751763

764+
class HookSpec(object):
765+
def __init__(self, namespace, name, hook_spec_opts):
766+
self.namespace = namespace
767+
self.function = function = getattr(namespace, name)
768+
self.name = name
769+
self.argnames, self.kwargnames = varnames(function)
770+
self.kwargvalues = inspect.getargspec(function).defaults
771+
self.kwargs = dict(
772+
zip(self.kwargnames, self.kwargvalues)
773+
) if self.kwargvalues else {}
774+
self.opts = hook_spec_opts
775+
self.argnames = ["__multicall__"] + list(self.argnames)
776+
777+
752778
class HookImpl(object):
753779
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
754780
self.function = function
755781
self.argnames, self.kwargnames = varnames(self.function)
782+
self.kwargvalues = inspect.getargspec(function).defaults
783+
self.kwargs = dict(
784+
zip(self.kwargnames, self.kwargvalues)
785+
) if self.kwargvalues else {}
756786
self.plugin = plugin
757787
self.opts = hook_impl_opts
758788
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)