From 4a21317becf7fc302b57061e3f98e9602d44955f 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/hooks.py | 30 ++++++++++++++++++------------ pluggy/manager.py | 14 +++++++++----- testing/benchmark.py | 2 +- testing/test_hookcaller.py | 6 +++--- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/pluggy/hooks.py b/pluggy/hooks.py index e50e0961..163f3224 100644 --- a/pluggy/hooks.py +++ b/pluggy/hooks.py @@ -202,28 +202,22 @@ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None) self._wrappers = [] self._nonwrappers = [] self._hookexec = hook_execute - self._specmodule_or_class = None self.argnames = None self.kwargnames = None self.multicall = _multicall - self.spec_opts = spec_opts or {} + self.spec = 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 self._specmodule_or_class is not None + return True if self.spec is not None else False 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.update(spec_opts) + self.spec = HookSpec(specmodule_or_class, self.name, spec_opts) if spec_opts.get("historic"): self._call_history = [] - self.warn_on_impl = spec_opts.get("warn_on_impl") def is_historic(self): return hasattr(self, "_call_history") @@ -273,8 +267,9 @@ 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(kwargs.keys()) + if self.spec: + notincall = set(self.spec.argnames) - set(['__multicall__']) - set( + kwargs.keys()) if notincall: warnings.warn( "Argument(s) {} which are declared in the hookspec " @@ -344,3 +339,14 @@ def __init__(self, plugin, plugin_name, function, hook_impl_opts): def __repr__(self): return "" % (self.plugin_name, self.plugin) + + +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) + self.warn_on_impl = opts.get("warn_on_impl") diff --git a/pluggy/manager.py b/pluggy/manager.py index 24c22a67..694d3b4f 100644 --- a/pluggy/manager.py +++ b/pluggy/manager.py @@ -56,7 +56,10 @@ def __init__(self, project_name, implprefix=None): ) self._implprefix = implprefix self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall( - methods, kwargs, firstresult=hook.spec_opts.get("firstresult") + methods, + kwargs, + firstresult=hook.spec.opts.get( + "firstresult") if hook.spec else False, ) def _hookexec(self, hook, methods, kwargs): @@ -215,10 +218,10 @@ def _verify_hook(self, hook, hookimpl): "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" % (hookimpl.plugin_name, hook.name), ) - if hook.warn_on_impl: - _warn_for_function(hook.warn_on_impl, hookimpl.function) + if hook.spec.warn_on_impl: + _warn_for_function(hook.spec.warn_on_impl, hookimpl.function) # positional arg checking - notinspec = set(hookimpl.argnames) - set(hook.argnames) + notinspec = set(hookimpl.argnames) - set(hook.spec.argnames) if notinspec: raise PluginValidationError( hookimpl.plugin, @@ -325,7 +328,8 @@ def subset_hook_caller(self, name, remove_plugins): 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 + orig.name, orig._hookexec, + orig.spec.namespace, orig.spec.opts ) for hookimpl in orig._wrappers + orig._nonwrappers: plugin = hookimpl.plugin diff --git a/testing/benchmark.py b/testing/benchmark.py index 46be3d01..aa8de929 100644 --- a/testing/benchmark.py +++ b/testing/benchmark.py @@ -15,7 +15,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_hookcaller.py b/testing/test_hookcaller.py index d16aff5e..5664f2bb 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -169,9 +169,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"])