Skip to content

Commit

Permalink
Merge pull request #545 from RonnyPfannschmidt/ronny/fix-544-wrapper-…
Browse files Browse the repository at this point in the history
…stop-iteration-passtrough

fix #544: Correctly pass StopIteration trough wrappers
  • Loading branch information
RonnyPfannschmidt authored Nov 12, 2024
2 parents 2b6dfd7 + 9cf2eaa commit 9d19d4b
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 2 deletions.
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

0 comments on commit 9d19d4b

Please sign in to comment.