diff --git a/changelog/798.bugfix.rst b/changelog/798.bugfix.rst new file mode 100644 index 00000000..f37760ab --- /dev/null +++ b/changelog/798.bugfix.rst @@ -0,0 +1 @@ +Ensure outer hook wrappers receive the error and complete teardown when a new-style hook wrapper yields more than once. diff --git a/src/pluggy/_execution.py b/src/pluggy/_execution.py index 0e62b4d3..f3511e1d 100644 --- a/src/pluggy/_execution.py +++ b/src/pluggy/_execution.py @@ -182,6 +182,7 @@ 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 @@ -189,7 +190,6 @@ def _multicall( except BaseException as e: exception = e continue - _raise_wrapfail(teardown, "has second yield") if exception is not None: raise exception diff --git a/testing/test_multicall.py b/testing/test_multicall.py index e400e85a..3df81e70 100644 --- a/testing/test_multicall.py +++ b/testing/test_multicall.py @@ -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 = []