Skip to content

Commit 9f5ea2e

Browse files
committed
Validate that a hookwrapper's function is a generator function during registration
Previously this error was only reported when calling the hook. Now it is reported during registration.
1 parent 656bc3d commit 9f5ea2e

File tree

4 files changed

+51
-1
lines changed

4 files changed

+51
-1
lines changed

changelog/282.feature.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
When registering a hookimpl which is declared as ``hookwrapper=True`` but whose
2+
function is not a generator function, a ``PluggyValidationError`` exception is
3+
now raised.
4+
5+
Previously this problem would cause an error only later, when calling the hook.
6+
7+
In the unlikely case that you have a hookwrapper that *returns* a generator
8+
instead of yielding directly, for example:
9+
10+
.. code-block:: python
11+
12+
def my_hook_real_implementation(arg):
13+
print("before")
14+
yield
15+
print("after")
16+
17+
@hookimpl(hookwrapper=True)
18+
def my_hook(arg):
19+
return my_hook_implementation(arg)
20+
21+
change it to use ``yield from`` instead:
22+
23+
.. code-block:: python
24+
25+
@hookimpl(hookwrapper=True)
26+
def my_hook(arg):
27+
yield from my_hook_implementation(arg)

src/pluggy/manager.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,10 @@ def _verify_hook(self, hook, hookimpl):
225225
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper"
226226
% (hookimpl.plugin_name, hook.name),
227227
)
228+
228229
if hook.spec.warn_on_impl:
229230
_warn_for_function(hook.spec.warn_on_impl, hookimpl.function)
231+
230232
# positional arg checking
231233
notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
232234
if notinspec:
@@ -243,6 +245,14 @@ def _verify_hook(self, hook, hookimpl):
243245
),
244246
)
245247

248+
if hookimpl.hookwrapper and not inspect.isgeneratorfunction(hookimpl.function):
249+
raise PluginValidationError(
250+
hookimpl.plugin,
251+
"Plugin %r for hook %r\nhookimpl definition: %s\n"
252+
"Declared as hookwrapper=True but function is not a generator function"
253+
% (hookimpl.plugin_name, hook.name, _formatdef(hookimpl.function)),
254+
)
255+
246256
def check_pending(self):
247257
""" Verify that all hooks which have not been verified against
248258
a hook specification are optional, otherwise raise :py:class:`.PluginValidationError`."""

testing/test_details.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def x1meth(self):
2121

2222
@hookimpl(hookwrapper=True, tryfirst=True)
2323
def x1meth2(self):
24-
pass
24+
yield
2525

2626
class Spec:
2727
@hookspec

testing/test_pluginmanager.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ def he_method1(self, qlwkje):
135135
assert excinfo.value.plugin is plugin
136136

137137

138+
def test_register_hookwrapper_not_a_generator_function(he_pm):
139+
class hello:
140+
@hookimpl(hookwrapper=True)
141+
def he_method1(self):
142+
pass
143+
144+
plugin = hello()
145+
146+
with pytest.raises(PluginValidationError, match="generator function") as excinfo:
147+
he_pm.register(plugin)
148+
assert excinfo.value.plugin is plugin
149+
150+
138151
def test_register(pm):
139152
class MyPlugin:
140153
pass

0 commit comments

Comments
 (0)