Skip to content

Commit

Permalink
Add defaults support to enable deprecation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Tyler Goodlet committed Nov 17, 2016
1 parent 9ab0ebe commit dd4641e
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 23 deletions.
64 changes: 45 additions & 19 deletions pluggy.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,8 @@ def __init__(self, project_name, implprefix=None):
self._implprefix = implprefix
self._inner_hookexec = lambda hook, methods, kwargs: \
_MultiCall(
methods, kwargs, specopts=hook.spec_opts, hook=hook
).execute()
methods, kwargs, firstresult=hook.spec.opts['firstresult'],
hook=hook).execute()

def _hookexec(self, hook, methods, kwargs):
# called from all hookcaller instances.
Expand Down Expand Up @@ -489,7 +489,7 @@ def _verify_hook(self, hook, hookimpl):
(hookimpl.plugin_name, hook.name))

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

def __init__(self, hook_impls, kwargs, specopts={}, hook=None):
def __init__(self, hook_impls, kwargs, firstresult=False, hook=None):
self.hook = hook
self.hook_impls = hook_impls
self.caller_kwargs = kwargs # come from _HookCaller.__call__()
self.caller_kwargs["__multicall__"] = self
self.specopts = hook.spec_opts if hook else specopts
self.firstresult = firstresult

def execute(self):
caller_kwargs = self.caller_kwargs
self.results = results = []
firstresult = self.specopts.get("firstresult")
firstresult = self.firstresult

while self.hook_impls:
hook_impl = self.hook_impls.pop()
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
# get any caller provided kwargs declared in our
# hookimpl and fail over to the spec's value if provided
if self.hook and hook_impl.kwargnames:
args += [
caller_kwargs.get(
argname, self.hook.spec.kwargs.get(
argname, hook_impl.kwargs[argname]))
for argname in hook_impl.kwargnames
]
except KeyError:
for argname in hook_impl.argnames:
if argname not in caller_kwargs:
raise HookCallError(
"hook call must provide argument %r" % (argname,))

if hook_impl.hookwrapper:
return _wrapped_call(hook_impl.function(*args), self.execute)

res = hook_impl.function(*args)
if res is not None:
if firstresult:
Expand Down Expand Up @@ -710,28 +721,23 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None)
self._wrappers = []
self._nonwrappers = []
self._hookexec = hook_execute
self.argnames = None
self.kwargnames = None
self.spec = None
self._call_history = None
if specmodule_or_class is not None:
assert spec_opts is not None
self.set_specification(specmodule_or_class, spec_opts)

def has_spec(self):
return hasattr(self, "_specmodule_or_class")
return self.spec is not None

def set_specification(self, specmodule_or_class, spec_opts):
assert not self.has_spec()
self._specmodule_or_class = specmodule_or_class
specfunc = getattr(specmodule_or_class, self.name)
# get spec arg signature
argnames, self.kwargnames = varnames(specfunc)
self.argnames = ["__multicall__"] + list(argnames)
self.spec_opts = spec_opts
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
if spec_opts.get("historic"):
self._call_history = []

def is_historic(self):
return hasattr(self, "_call_history")
return self._call_history is not None

def _remove_plugin(self, plugin):
def remove(wrappers):
Expand Down Expand Up @@ -767,7 +773,7 @@ def __repr__(self):

def __call__(self, **kwargs):
assert not self.is_historic()
notincall = set(self.argnames) - set(kwargs.keys())
notincall = set(self.spec.argnames) - set(kwargs.keys())
if notincall:
warnings.warn(
"Positional arg(s) %s are declared in the hookspec "
Expand Down Expand Up @@ -812,10 +818,30 @@ def _maybe_apply_history(self, method):
proc(res[0])


class HookSpec:
def __init__(self, namespace, name, hook_spec_opts):
self.namespace = namespace
self.function = function = getattr(namespace, name)
self.name = name
self.argnames, self.kwargnames = varnames(function)
self.kwargvalues = inspect.getargspec(function).defaults
self.kwargs = dict(
((name, value) for name, value in
zip(self.kwargnames, inspect.getargspec(function).defaults))
) if self.kwargvalues else {}
self.opts = hook_spec_opts
self.argnames = ["__multicall__"] + list(self.argnames)


class HookImpl:
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
self.function = function
self.argnames, self.kwargnames = varnames(self.function)
self.kwargvalues = inspect.getargspec(function).defaults
self.kwargs = dict(
((name, value) for name, value in
zip(self.kwargnames, inspect.getargspec(function).defaults))
) if self.kwargvalues else {}
self.plugin = plugin
self.opts = hook_impl_opts
self.plugin_name = plugin_name
Expand Down
8 changes: 4 additions & 4 deletions testing/test_pluggy.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,9 +500,9 @@ def he_myhook3(self, arg1):
pass

pm.add_hookspecs(HookSpec)
assert not pm.hook.he_myhook1.spec_opts["firstresult"]
assert pm.hook.he_myhook2.spec_opts["firstresult"]
assert not pm.hook.he_myhook3.spec_opts["firstresult"]
assert not pm.hook.he_myhook1.spec.opts['firstresult']
assert pm.hook.he_myhook2.spec.opts['firstresult']
assert not pm.hook.he_myhook3.spec.opts['firstresult']

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

def test_call_passing(self):
class P1:
Expand Down

0 comments on commit dd4641e

Please sign in to comment.