Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/798.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ensure outer hook wrappers receive the error and complete teardown when a new-style hook wrapper yields more than once.
2 changes: 1 addition & 1 deletion src/pluggy/_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,14 @@ def _multicall(
# Try to force finalizers otherwise postponed till GC action.
# Note: close() may raise if generator handles GeneratorExit.
teardown.close()
_raise_wrapfail(teardown, "has second yield")
except StopIteration as si:
result = si.value
exception = None
continue
except BaseException as e:
exception = e
continue
_raise_wrapfail(teardown, "has second yield")

if exception is not None:
raise exception
Expand Down
50 changes: 50 additions & 0 deletions testing/test_multicall.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,56 @@ def m():
MC([m], {})


@pytest.mark.parametrize("old_style_outer", [False, True])
@pytest.mark.parametrize("hook_raises", [False, True])
def test_wrapper_two_yields_unwinds_outer_wrappers(
old_style_outer: bool, hook_raises: bool
) -> None:
out = []
errors = []

@hookimpl
def impl():
if hook_raises:
raise ValueError("hook failed")
return 1

@hookimpl(wrapper=True)
def inner():
try:
try:
yield
except ValueError:
pass
yield
finally:
out.append("inner cleanup")

@hookimpl(wrapper=not old_style_outer, hookwrapper=old_style_outer)
def outer():
try:
if old_style_outer:
outcome = yield
errors.append(outcome.exception)
else:
try:
yield
except RuntimeError as exc:
errors.append(exc)
raise
finally:
out.append("outer cleanup")

with pytest.raises(RuntimeError, match="has second yield") as excinfo:
try:
MC([impl, inner, outer], {})
finally:
out.append("caller cleanup")

assert errors == [excinfo.value]
assert out == ["inner cleanup", "outer cleanup", "caller cleanup"]


def test_hookwrapper_order() -> None:
out = []

Expand Down
Loading