Skip to content

Commit

Permalink
fix #544: Correctly pass StopIteration trough wrappers
Browse files Browse the repository at this point in the history
Raising a StopIteration in a generator triggers a RuntimeError.
If the RuntimeError of a generator has the passed in StopIteration as cause
resume with that StopIteration as normal exception instead of failing with the RuntimeError.
  • Loading branch information
RonnyPfannschmidt committed Nov 3, 2024
1 parent 145eea8 commit eb49a73
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
):

Check warning on line 130 in src/pluggy/_callers.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_callers.py#L130

Added line #L130 was not covered by tests
teardown.close() # type: ignore[union-attr]
continue
else:
raise

Check warning on line 134 in src/pluggy/_callers.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_callers.py#L133-L134

Added lines #L133 - L134 were not covered by tests
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

Check warning on line 182 in src/pluggy/_callers.py

View check run for this annotation

Codecov / codecov/patch

src/pluggy/_callers.py#L182

Added line #L182 was not covered by tests
# 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 eb49a73

Please sign in to comment.