From 3a8dc7617f8dd7f8e26a2524a8a38b6fe4bc38a2 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 7 Jul 2017 18:55:18 -0400 Subject: [PATCH] Encapsulate spec definitions with a class Allows for easier introspection of spec definitions including function signatures and hook options. Originally introduced to address #15 and the accompanying PR (#43) which requires keeping track of spec default arguments values. --- pluggy/__init__.py | 38 +++++++++++++++++++++------------ testing/benchmark.py | 2 +- testing/test_method_ordering.py | 6 +++--- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/pluggy/__init__.py b/pluggy/__init__.py index 46011b8e..27d9632a 100644 --- a/pluggy/__init__.py +++ b/pluggy/__init__.py @@ -212,7 +212,10 @@ def __init__(self, project_name, implprefix=None): self._implprefix = implprefix self._inner_hookexec = lambda hook, methods, kwargs: \ hook.multicall( - methods, kwargs, specopts=hook.spec_opts, hook=hook + methods, + kwargs, + specopts=hook.spec.opts['firstresult'] if hook.spec else False, + hook=hook ) def _hookexec(self, hook, methods, kwargs): @@ -360,7 +363,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" @@ -453,8 +456,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: @@ -536,26 +539,23 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None) self.argnames = None self.kwargnames = None self.multicall = _multicall + 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): @@ -601,8 +601,8 @@ def __call__(self, *args, **kwargs): if args: raise TypeError("hook calling supports only keyword arguments") assert not self.is_historic() - if self.argnames: - notincall = set(self.argnames) - set(['__multicall__']) - set( + if self.spec: + notincall = set(self.spec.argnames) - set(['__multicall__']) - set( kwargs.keys()) if notincall: warnings.warn( @@ -649,6 +649,16 @@ def _maybe_apply_history(self, method): proc(res[0]) +class HookSpec(object): + def __init__(self, namespace, name, opts): + self.namespace = namespace + self.function = function = getattr(namespace, name) + self.name = name + self.argnames, self.kwargnames = varnames(function) + self.opts = opts + self.argnames = ["__multicall__"] + list(self.argnames) + + class HookImpl(object): def __init__(self, plugin, plugin_name, function, hook_impl_opts): self.function = function diff --git a/testing/benchmark.py b/testing/benchmark.py index 5a913e9d..802db264 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -14,7 +14,7 @@ def MC(methods, kwargs, callertype, firstresult=False): for method in methods: f = HookImpl(None, "", method, method.example_impl) hookfuncs.append(f) - return callertype(hookfuncs, kwargs, {"firstresult": firstresult}) + return callertype(hookfuncs, kwargs, firstresult=firstresult) @hookimpl diff --git a/testing/test_method_ordering.py b/testing/test_method_ordering.py index 168bc562..4e88c903 100644 --- a/testing/test_method_ordering.py +++ b/testing/test_method_ordering.py @@ -163,9 +163,9 @@ def he_myhook3(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"] @pytest.mark.parametrize('name', ["hookwrapper", "optionalhook", "tryfirst", "trylast"])