Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #544: Correctly pass StopIteration trough wrappers #545

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 @@
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 (
RonnyPfannschmidt marked this conversation as resolved.
Show resolved Hide resolved
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 @@
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