Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add specname option to hookimpl #251

Merged
merged 11 commits into from
Feb 5, 2020
6 changes: 6 additions & 0 deletions src/pluggy/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def __call__(
optionalhook=False,
tryfirst=False,
trylast=False,
specname=None,
):

""" if passed a function, directly sets attributes on the function
Expand All @@ -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 this hook implementation to a hook specification during registration.

"""

def setattr_hookimpl_opts(func):
Expand All @@ -107,6 +111,7 @@ def setattr_hookimpl_opts(func):
optionalhook=optionalhook,
tryfirst=tryfirst,
trylast=trylast,
specname=specname,
),
)
return func
Expand All @@ -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"):
Expand Down
1 change: 1 addition & 0 deletions src/pluggy/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +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)
name = hookimpl_opts.get("specname") or name
hook = getattr(self.hook, name, None)
if hook is None:
hook = _HookCaller(name, self._hookexec)
Expand Down
47 changes: 46 additions & 1 deletion testing/test_hookcaller.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -213,3 +213,48 @@ def hello(self, arg):
assert not hasattr(hook, "world")
pm.unregister(plugin)
assert hook.hello(arg=3) == []


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 = Plugin()
pm.register(plugin)
out = hook.hello(arg=3)
tlambert03 marked this conversation as resolved.
Show resolved Hide resolved
assert out == [4]
tlambert03 marked this conversation as resolved.
Show resolved Hide resolved

# 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):
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())
tlambert03 marked this conversation as resolved.
Show resolved Hide resolved
pm.check_pending()