From 9fddd543a093ad49b25354e4a07e1a0171742cc4 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 26 Jan 2020 19:24:04 -0500 Subject: [PATCH 01/11] add specname option to hookimpl --- src/pluggy/hooks.py | 6 ++++++ src/pluggy/manager.py | 7 ++++--- testing/test_hookcaller.py | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/pluggy/hooks.py b/src/pluggy/hooks.py index 0a1c2871..0f4195d6 100644 --- a/src/pluggy/hooks.py +++ b/src/pluggy/hooks.py @@ -73,6 +73,7 @@ def __call__( optionalhook=False, tryfirst=False, trylast=False, + specname=None, ): """ if passed a function, directly sets attributes on the function @@ -96,6 +97,9 @@ def __call__( representing the exception or result outcome of the inner calls (including other hookwrapper calls). + If ``specname`` is provided, it will be used instead of the function name when + matching the this hook implementation with a hook specification during registration. + """ def setattr_hookimpl_opts(func): @@ -107,6 +111,7 @@ def setattr_hookimpl_opts(func): optionalhook=optionalhook, tryfirst=tryfirst, trylast=trylast, + specname=specname, ), ) return func @@ -122,6 +127,7 @@ def normalize_hookimpl_opts(opts): opts.setdefault("trylast", False) opts.setdefault("hookwrapper", False) opts.setdefault("optionalhook", False) + opts.setdefault("specname", None) if hasattr(inspect, "getfullargspec"): diff --git a/src/pluggy/manager.py b/src/pluggy/manager.py index 07b42cba..41c27f3d 100644 --- a/src/pluggy/manager.py +++ b/src/pluggy/manager.py @@ -118,10 +118,11 @@ def register(self, plugin, name=None): normalize_hookimpl_opts(hookimpl_opts) method = getattr(plugin, name) hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) - hook = getattr(self.hook, name, None) + specname = hookimpl_opts.get('specname') or name + hook = getattr(self.hook, specname, None) if hook is None: - hook = _HookCaller(name, self._hookexec) - setattr(self.hook, name, hook) + hook = _HookCaller(specname, self._hookexec) + setattr(self.hook, specname, hook) elif hook.has_spec(): self._verify_hook(hook, hookimpl) hook._maybe_apply_history(hookimpl) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 5664f2bb..4a982196 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -174,7 +174,9 @@ def he_myhook3(arg1): assert not pm.hook.he_myhook3.spec.opts["firstresult"] -@pytest.mark.parametrize("name", ["hookwrapper", "optionalhook", "tryfirst", "trylast"]) +@pytest.mark.parametrize( + "name", ["hookwrapper", "optionalhook", "tryfirst", "trylast", "specname"] +) @pytest.mark.parametrize("val", [True, False]) def test_hookimpl(name, val): @hookimpl(**{name: val}) @@ -213,3 +215,15 @@ def hello(self, arg): assert not hasattr(hook, "world") pm.unregister(plugin) assert hook.hello(arg=3) == [] + + # the `specname` argument overrides the function name when registering a hook caller + class Plugin2(object): + @hookimpl(specname="hello") + def foo(self, arg): + return arg + 1 + + plugin = Plugin2() + pm.register(plugin) + out = hook.hello(arg=3) + assert out == [4] + assert not hasattr(hook, "world") \ No newline at end of file From 372fc9251817e12ee6054f6575198c0c8c145e6a Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 26 Jan 2020 19:26:34 -0500 Subject: [PATCH 02/11] typo --- src/pluggy/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pluggy/hooks.py b/src/pluggy/hooks.py index 0f4195d6..79d246ac 100644 --- a/src/pluggy/hooks.py +++ b/src/pluggy/hooks.py @@ -98,7 +98,7 @@ def __call__( hookwrapper calls). If ``specname`` is provided, it will be used instead of the function name when - matching the this hook implementation with a hook specification during registration. + matching this hook implementation to a hook specification during registration. """ From a568a9fe545df0c7e5310bf1fd7dd70f19ca7a30 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 26 Jan 2020 19:31:53 -0500 Subject: [PATCH 03/11] add newline --- testing/test_hookcaller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 4a982196..46d70c71 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -226,4 +226,3 @@ def foo(self, arg): pm.register(plugin) out = hook.hello(arg=3) assert out == [4] - assert not hasattr(hook, "world") \ No newline at end of file From a3b11f74b4e55a2c8d171646a608756a6a551b36 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 26 Jan 2020 19:48:06 -0500 Subject: [PATCH 04/11] fix black double-quotes --- src/pluggy/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pluggy/manager.py b/src/pluggy/manager.py index 41c27f3d..05b30dd1 100644 --- a/src/pluggy/manager.py +++ b/src/pluggy/manager.py @@ -118,7 +118,7 @@ def register(self, plugin, name=None): normalize_hookimpl_opts(hookimpl_opts) method = getattr(plugin, name) hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) - specname = hookimpl_opts.get('specname') or name + specname = hookimpl_opts.get("specname") or name hook = getattr(self.hook, specname, None) if hook is None: hook = _HookCaller(specname, self._hookexec) From a10dbae8ffd427f56cc0e1f4445615a1814d6baf Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 27 Jan 2020 07:58:44 -0500 Subject: [PATCH 05/11] fix tests --- testing/test_hookcaller.py | 39 ++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 46d70c71..a0e72e65 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -174,9 +174,7 @@ def he_myhook3(arg1): assert not pm.hook.he_myhook3.spec.opts["firstresult"] -@pytest.mark.parametrize( - "name", ["hookwrapper", "optionalhook", "tryfirst", "trylast", "specname"] -) +@pytest.mark.parametrize("name", ["hookwrapper", "optionalhook", "tryfirst", "trylast"]) @pytest.mark.parametrize("val", [True, False]) def test_hookimpl(name, val): @hookimpl(**{name: val}) @@ -189,6 +187,21 @@ def he_myhook1(arg1): assert not hasattr(he_myhook1, name) +def test_hookimpl_specname(): + """Make sure functions decorated with specname get the appropriate tag""" + + @hookimpl() + def he_myhook1(arg1): + pass + + @hookimpl(specname="name") + def he_myhook2(arg1): + pass + + assert he_myhook1.example_impl.get("specname") is None + assert he_myhook2.example_impl.get("specname") == "name" + + def test_hookrelay_registry(pm): """Verify hook caller instances are registered by name onto the relay and can be likewise unregistered.""" @@ -216,13 +229,27 @@ def hello(self, arg): pm.unregister(plugin) assert hook.hello(arg=3) == [] - # the `specname` argument overrides the function name when registering a hook caller - class Plugin2(object): + +def test_hookrelay_registration_by_specname(pm): + """Verify hook caller instances may also be registered by specifying a + specname option to the hookimpl""" + + class Api(object): + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + hook = pm.hook + assert hasattr(hook, "hello") + assert len(pm.hook.hello.get_hookimpls()) == 0 + + class Plugin(object): @hookimpl(specname="hello") def foo(self, arg): return arg + 1 - plugin = Plugin2() + plugin = Plugin() pm.register(plugin) out = hook.hello(arg=3) assert out == [4] From 306639844ef9bc12f6519aa71413242ba2f81d56 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 5 Feb 2020 07:39:15 -0500 Subject: [PATCH 06/11] remove unnecessary test --- testing/test_hookcaller.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index a0e72e65..2aa86c9d 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -187,21 +187,6 @@ def he_myhook1(arg1): assert not hasattr(he_myhook1, name) -def test_hookimpl_specname(): - """Make sure functions decorated with specname get the appropriate tag""" - - @hookimpl() - def he_myhook1(arg1): - pass - - @hookimpl(specname="name") - def he_myhook2(arg1): - pass - - assert he_myhook1.example_impl.get("specname") is None - assert he_myhook2.example_impl.get("specname") == "name" - - def test_hookrelay_registry(pm): """Verify hook caller instances are registered by name onto the relay and can be likewise unregistered.""" From 64ec41f45bc885508e98ef51be6c6c45ce42bd35 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 5 Feb 2020 07:39:37 -0500 Subject: [PATCH 07/11] return local specname var to name --- src/pluggy/manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pluggy/manager.py b/src/pluggy/manager.py index 05b30dd1..22dd78d6 100644 --- a/src/pluggy/manager.py +++ b/src/pluggy/manager.py @@ -118,11 +118,11 @@ def register(self, plugin, name=None): normalize_hookimpl_opts(hookimpl_opts) method = getattr(plugin, name) hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) - specname = hookimpl_opts.get("specname") or name - hook = getattr(self.hook, specname, None) + name = hookimpl_opts.get("specname") or name + hook = getattr(self.hook, name, None) if hook is None: - hook = _HookCaller(specname, self._hookexec) - setattr(self.hook, specname, hook) + hook = _HookCaller(name, self._hookexec) + setattr(self.hook, name, hook) elif hook.has_spec(): self._verify_hook(hook, hookimpl) hook._maybe_apply_history(hookimpl) From a57f279592c32b291a523c1d14defc3eeda08c8a Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 5 Feb 2020 07:50:45 -0500 Subject: [PATCH 08/11] assert matching signature with specname option --- testing/test_hookcaller.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 2aa86c9d..307bd880 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -1,6 +1,6 @@ import pytest -from pluggy import HookimplMarker, HookspecMarker +from pluggy import HookimplMarker, HookspecMarker, PluginValidationError from pluggy.hooks import HookImpl hookspec = HookspecMarker("example") @@ -238,3 +238,12 @@ def foo(self, arg): pm.register(plugin) out = hook.hello(arg=3) assert out == [4] + + class Plugin2(object): + @hookimpl(specname="hello") + def foo(self, arg, too, many, args): + return arg + 1 + + with pytest.raises(PluginValidationError): + plugin2 = Plugin2() + pm.register(plugin2) \ No newline at end of file From ef768f28d572b83e068adfd3789183c712291042 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 5 Feb 2020 07:55:38 -0500 Subject: [PATCH 09/11] add check_pending test for specname --- testing/test_hookcaller.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 307bd880..662d5fea 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -239,11 +239,22 @@ def foo(self, arg): out = hook.hello(arg=3) assert out == [4] + # make sure a bad signature still raises an error when using specname class Plugin2(object): @hookimpl(specname="hello") def foo(self, arg, too, many, args): return arg + 1 with pytest.raises(PluginValidationError): - plugin2 = Plugin2() - pm.register(plugin2) \ No newline at end of file + pm.register(Plugin2()) + + # make sure check_pending still fails if specname doesn't have a + # corresponding spec. EVEN if the function name matches one. + class Plugin3(object): + @hookimpl(specname="bar") + def hello(self, arg): + return arg + 1 + + with pytest.raises(PluginValidationError): + pm.register(Plugin3()) + pm.check_pending() From dbb1c44495f01568b7a572fc7eb1ae866c75991f Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 5 Feb 2020 12:34:46 -0500 Subject: [PATCH 10/11] tweak tests --- testing/test_hookcaller.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index 662d5fea..ff29176e 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -239,22 +239,34 @@ def foo(self, arg): out = hook.hello(arg=3) assert out == [4] + +def test_hookrelay_registration_by_specname_raises(pm): + """Verify using specname still raises the types of errors during registration as it + would have without using specname.""" + + class Api(object): + @hookspec + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + # make sure a bad signature still raises an error when using specname - class Plugin2(object): + class Plugin(object): @hookimpl(specname="hello") def foo(self, arg, too, many, args): return arg + 1 with pytest.raises(PluginValidationError): - pm.register(Plugin2()) + pm.register(Plugin()) # make sure check_pending still fails if specname doesn't have a # corresponding spec. EVEN if the function name matches one. - class Plugin3(object): + class Plugin2(object): @hookimpl(specname="bar") def hello(self, arg): return arg + 1 + pm.register(Plugin2()) with pytest.raises(PluginValidationError): - pm.register(Plugin3()) pm.check_pending() From e37c8d0c4e6609d58b5b0875a4d431613cd21636 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 5 Feb 2020 12:42:45 -0500 Subject: [PATCH 11/11] trailing whitespace --- testing/test_hookcaller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_hookcaller.py b/testing/test_hookcaller.py index ff29176e..cc345d4c 100644 --- a/testing/test_hookcaller.py +++ b/testing/test_hookcaller.py @@ -241,7 +241,7 @@ def foo(self, arg): def test_hookrelay_registration_by_specname_raises(pm): - """Verify using specname still raises the types of errors during registration as it + """Verify using specname still raises the types of errors during registration as it would have without using specname.""" class Api(object):