Skip to content

Commit e77787b

Browse files
committed
hooks: also verify kwargs are per spec in call_historic
1 parent 4576615 commit e77787b

File tree

2 files changed

+33
-31
lines changed

2 files changed

+33
-31
lines changed

src/pluggy/_hooks.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -346,27 +346,32 @@ def _add_hookimpl(self, hookimpl: "HookImpl") -> None:
346346
def __repr__(self) -> str:
347347
return f"<_HookCaller {self.name!r}>"
348348

349-
def __call__(self, *args: object, **kwargs: object) -> Any:
350-
if args:
351-
raise TypeError("hook calling supports only keyword arguments")
352-
assert not self.is_historic()
353-
349+
def _verify_all_args_are_provided(self, kwargs: Mapping[str, object]) -> None:
354350
# This is written to avoid expensive operations when not needed.
355351
if self.spec:
356352
for argname in self.spec.argnames:
357353
if argname not in kwargs:
358-
notincall = tuple(set(self.spec.argnames) - kwargs.keys())
354+
notincall = ", ".join(
355+
repr(argname)
356+
for argname in self.spec.argnames
357+
# Avoid self.spec.argnames - kwargs.keys() - doesn't preserve order.
358+
if argname not in kwargs.keys()
359+
)
359360
warnings.warn(
360361
"Argument(s) {} which are declared in the hookspec "
361-
"can not be found in this hook call".format(notincall),
362+
"cannot be found in this hook call".format(notincall),
362363
stacklevel=2,
363364
)
364365
break
365366

366-
firstresult = self.spec.opts.get("firstresult", False)
367-
else:
368-
firstresult = False
369-
367+
def __call__(self, *args: object, **kwargs: object) -> Any:
368+
if args:
369+
raise TypeError("hook calling supports only keyword arguments")
370+
assert (
371+
not self.is_historic()
372+
), "Cannot directly call a historic hook - use call_historic instead."
373+
self._verify_all_args_are_provided(kwargs)
374+
firstresult = self.spec.opts.get("firstresult", False) if self.spec else False
370375
return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
371376

372377
def call_historic(
@@ -382,6 +387,7 @@ def call_historic(
382387
"""
383388
assert self._call_history is not None
384389
kwargs = kwargs or {}
390+
self._verify_all_args_are_provided(kwargs)
385391
self._call_history.append((kwargs, result_callback))
386392
# Historizing hooks don't return results.
387393
# Remember firstresult isn't compatible with historic.

testing/test_details.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import warnings
21
import pytest
32
from pluggy import PluginManager, HookimplMarker, HookspecMarker
43

@@ -89,36 +88,33 @@ class Module:
8988
assert pm.get_plugin("donttouch") is module
9089

9190

92-
def test_warning_on_call_vs_hookspec_arg_mismatch() -> None:
93-
"""Verify that is a hook is called with less arguments then defined in the
94-
spec that a warning is emitted.
95-
"""
91+
def test_not_all_arguments_are_provided_issues_a_warning(pm: PluginManager) -> None:
92+
"""Calling a hook without providing all arguments specified in
93+
the hook spec issues a warning."""
9694

9795
class Spec:
9896
@hookspec
99-
def myhook(self, arg1, arg2):
97+
def hello(self, arg1, arg2):
10098
pass
10199

102-
class Plugin:
103-
@hookimpl
104-
def myhook(self, arg1):
100+
@hookspec(historic=True)
101+
def herstory(self, arg1, arg2):
105102
pass
106103

107-
pm = PluginManager(hookspec.project_name)
108-
pm.register(Plugin())
109104
pm.add_hookspecs(Spec)
110105

111-
with warnings.catch_warnings(record=True) as warns:
112-
warnings.simplefilter("always")
106+
with pytest.warns(UserWarning, match=r"'arg1', 'arg2'.*cannot be found.*$"):
107+
pm.hook.hello()
108+
with pytest.warns(UserWarning, match=r"'arg2'.*cannot be found.*$"):
109+
pm.hook.hello(arg1=1)
110+
with pytest.warns(UserWarning, match=r"'arg1'.*cannot be found.*$"):
111+
pm.hook.hello(arg2=2)
113112

114-
# calling should trigger a warning
115-
pm.hook.myhook(arg1=1)
113+
with pytest.warns(UserWarning, match=r"'arg1', 'arg2'.*cannot be found.*$"):
114+
pm.hook.hello.call_extra([], kwargs=dict())
116115

117-
assert warns is not None
118-
assert len(warns) == 1
119-
warning = warns[-1]
120-
assert issubclass(warning.category, Warning)
121-
assert "Argument(s) ('arg2',)" in str(warning.message)
116+
with pytest.warns(UserWarning, match=r"'arg1', 'arg2'.*cannot be found.*$"):
117+
pm.hook.herstory.call_historic(kwargs=dict())
122118

123119

124120
def test_repr() -> None:

0 commit comments

Comments
 (0)