Skip to content
Merged
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
6 changes: 6 additions & 0 deletions changelog/544.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Correctly pass :class:`StopIteration` trough hook wrappers.

Raising a :class:`StopIteration` in a generator triggers a :class:`RuntimeError`.

If the :class:`RuntimeError` of a generator has the passed in :class:`StopIteration` as cause
resume with that :class:`StopIteration` as normal exception instead of failing with the :class:`RuntimeError`.
28 changes: 26 additions & 2 deletions src/pluggy/_callers.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,19 @@ def _multicall(
for teardown in reversed(teardowns):
try:
if exception is not None:
teardown.throw(exception) # type: ignore[union-attr]
try:
teardown.throw(exception) # type: ignore[union-attr]
except RuntimeError as re:
# StopIteration from generator causes RuntimeError
# even for coroutine usage - see #544
if (
isinstance(exception, StopIteration)
and re.__cause__ is exception
):
teardown.close() # type: ignore[union-attr]
continue
else:
raise
else:
teardown.send(result) # type: ignore[union-attr]
# Following is unreachable for a well behaved hook wrapper.
Expand Down Expand Up @@ -164,7 +176,19 @@ def _multicall(
else:
try:
if outcome._exception is not None:
teardown.throw(outcome._exception)
try:
teardown.throw(outcome._exception)
except RuntimeError as re:
# StopIteration from generator causes RuntimeError
# even for coroutine usage - see #544
if (
isinstance(outcome._exception, StopIteration)
and re.__cause__ is outcome._exception
):
teardown.close()
continue
else:
raise
else:
teardown.send(outcome._result)
# Following is unreachable for a well behaved hook wrapper.
Expand Down
30 changes: 30 additions & 0 deletions testing/test_multicall.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,36 @@ def m2():
]


@pytest.mark.parametrize("has_hookwrapper", [True, False])
def test_wrapper_stopiteration_passtrough(has_hookwrapper: bool) -> None:
out = []

@hookimpl(wrapper=True)
def wrap():
out.append("wrap")
try:
yield
finally:
out.append("wrap done")

@hookimpl(wrapper=not has_hookwrapper, hookwrapper=has_hookwrapper)
def wrap_path2():
yield

@hookimpl
def stop():
out.append("stop")
raise StopIteration

with pytest.raises(StopIteration):
try:
MC([stop, wrap, wrap_path2], {})
finally:
out.append("finally")

assert out == ["wrap", "stop", "wrap done", "finally"]


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

Expand Down