Skip to content

Commit ac351e0

Browse files
authored
Merge pull request #282 from bluetech/hookwraper-gen-validate
Validate that a hookwrapper's function is a generator function during registration
2 parents 39bac1f + 204737e commit ac351e0

File tree

4 files changed

+52
-1
lines changed

4 files changed

+52
-1
lines changed

changelog/282.feature.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
18+
@hookimpl(hookwrapper=True)
19+
def my_hook(arg):
20+
return my_hook_implementation(arg)
21+
22+
change it to use ``yield from`` instead:
23+
24+
.. code-block:: python
25+
26+
@hookimpl(hookwrapper=True)
27+
def my_hook(arg):
28+
yield from my_hook_implementation(arg)

src/pluggy/manager.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,10 @@ def _verify_hook(self, hook, hookimpl):
221221
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper"
222222
% (hookimpl.plugin_name, hook.name),
223223
)
224+
224225
if hook.spec.warn_on_impl:
225226
_warn_for_function(hook.spec.warn_on_impl, hookimpl.function)
227+
226228
# positional arg checking
227229
notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
228230
if notinspec:
@@ -239,6 +241,14 @@ def _verify_hook(self, hook, hookimpl):
239241
),
240242
)
241243

244+
if hookimpl.hookwrapper and not inspect.isgeneratorfunction(hookimpl.function):
245+
raise PluginValidationError(
246+
hookimpl.plugin,
247+
"Plugin %r for hook %r\nhookimpl definition: %s\n"
248+
"Declared as hookwrapper=True but function is not a generator function"
249+
% (hookimpl.plugin_name, hook.name, _formatdef(hookimpl.function)),
250+
)
251+
242252
def check_pending(self):
243253
""" Verify that all hooks which have not been verified against
244254
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 # pragma: no cover
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 # pragma: no cover
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)