Skip to content

Commit eb70fb9

Browse files
epxand test coverage and drop dead code for wrappers
1 parent d0ab361 commit eb70fb9

File tree

4 files changed

+92
-13
lines changed

4 files changed

+92
-13
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ exclude_lines =
3030
^\s*\.\.\.\s*$
3131
# ignore coverage on ruff line continued
3232
^\s*def.*:\ \.\.\.\s*$
33+
.*: ...$
3334
# ignore coverage on pass lines
3435
^\s*passs*$

src/pluggy/_callers.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,25 +93,20 @@ def _multicall(
9393
for hook_impl in reversed(hook_impls):
9494
try:
9595
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
96-
except KeyError:
97-
for argname in hook_impl.argnames:
96+
except KeyError as e:
97+
# coverage bug - this is tested
98+
for argname in hook_impl.argnames: # pragma: no cover
9899
if argname not in caller_kwargs:
99100
raise HookCallError(
100101
f"hook call must provide argument {argname!r}"
101-
)
102+
) from e
102103

103104
if hook_impl.hookwrapper:
104-
try:
105-
# If this cast is not valid, a type error is raised below,
106-
# which is the desired response.
107-
function_gen = run_old_style_hookwrapper(
108-
hook_impl, hook_name, args
109-
)
105+
function_gen = run_old_style_hookwrapper(hook_impl, hook_name, args)
106+
107+
next(function_gen) # first yield
108+
teardowns.append(function_gen)
110109

111-
next(function_gen) # first yield
112-
teardowns.append(function_gen)
113-
except StopIteration:
114-
_raise_wrapfail(function_gen, "did not yield")
115110
elif hook_impl.wrapper:
116111
try:
117112
# If this cast is not valid, a type error is raised below,

testing/test_details.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
import pluggy
56
from pluggy import HookimplMarker
67
from pluggy import HookspecMarker
78
from pluggy import PluginManager
@@ -202,3 +203,44 @@ def test_dist_facade_list_attributes() -> None:
202203
res = dir(fc)
203204
assert res == sorted(res)
204205
assert set(res) - set(dir(fc._dist)) == {"_dist", "project_name"}
206+
207+
208+
def test_hookimpl_disallow_invalid_combination() -> None:
209+
decorator = hookspec(historic=True, firstresult=True)
210+
with pytest.raises(ValueError, match="cannot have a historic firstresult hook"):
211+
decorator(any)
212+
213+
214+
def test_hook_nonspec_call(pm: PluginManager) -> None:
215+
class Plugin:
216+
@hookimpl
217+
def a_hook(self, passed: str, missing: int) -> None:
218+
pass
219+
220+
pm.register(Plugin())
221+
with pytest.raises(
222+
pluggy.HookCallError, match="hook call must provide argument 'missing'"
223+
):
224+
pm.hook.a_hook(passed="a")
225+
pm.hook.a_hook(passed="a", missing="ok")
226+
227+
228+
def test_wrapper_runtimeerror_passtrough(pm: PluginManager) -> None:
229+
"""
230+
ensure runtime-error passes trough a wrapper in case of exceptions
231+
"""
232+
233+
class Fail:
234+
@hookimpl
235+
def fail_late(self):
236+
raise RuntimeError("this is personal")
237+
238+
class Plugin:
239+
@hookimpl(wrapper=True)
240+
def fail_late(self):
241+
yield
242+
243+
pm.register(Plugin())
244+
pm.register(Fail())
245+
with pytest.raises(RuntimeError, match="this is personal"):
246+
pm.hook.fail_late()

testing/test_invocations.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections.abc import Iterator
2+
from typing import Any
23

34
import pytest
45

@@ -326,3 +327,43 @@ def hello(self):
326327
pm.register(Plugin3())
327328
res = pm.hook.hello()
328329
assert [y for x in res for y in x] == [2, 3, 1]
330+
331+
332+
@pytest.mark.parametrize(
333+
"kind",
334+
[
335+
pytest.param(hookimpl(wrapper=True), id="wrapper"),
336+
pytest.param(hookimpl(hookwrapper=True), id="legacy-wrapper"),
337+
],
338+
)
339+
def test_wrappers_yield_twice_fails(pm: PluginManager, kind: Any) -> None:
340+
class Plugin:
341+
@kind
342+
def wrap(self):
343+
yield
344+
yield
345+
346+
pm.register(Plugin())
347+
with pytest.raises(
348+
RuntimeError, match="wrap_controller at 'wrap'.* has second yield"
349+
):
350+
pm.hook.wrap()
351+
352+
353+
@pytest.mark.parametrize(
354+
"kind",
355+
[
356+
pytest.param(hookimpl(wrapper=True), id="wrapper"),
357+
pytest.param(hookimpl(hookwrapper=True), id="legacy-wrapper"),
358+
],
359+
)
360+
def test_wrappers_yield_never_fails(pm: PluginManager, kind: Any) -> None:
361+
class Plugin:
362+
@kind
363+
def wrap(self):
364+
if False:
365+
yield # type: ignore[unreachable]
366+
367+
pm.register(Plugin())
368+
with pytest.raises(RuntimeError, match="wrap_controller at 'wrap'.* did not yield"):
369+
pm.hook.wrap()

0 commit comments

Comments
 (0)