diff --git a/changelog/544.bugfix.rst b/changelog/544.bugfix.rst new file mode 100644 index 00000000..6e7fe018 --- /dev/null +++ b/changelog/544.bugfix.rst @@ -0,0 +1,5 @@ +Correctly pass StopIteration trough wrappers. + +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. diff --git a/src/pluggy/_callers.py b/src/pluggy/_callers.py index f4a2aced..6b5b90e5 100644 --- a/src/pluggy/_callers.py +++ b/src/pluggy/_callers.py @@ -119,7 +119,17 @@ 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: + 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. diff --git a/testing/test_multicall.py b/testing/test_multicall.py index 7d8d8f28..231dbf44 100644 --- a/testing/test_multicall.py +++ b/testing/test_multicall.py @@ -416,6 +416,31 @@ def m2(): ] +def test_wrapper_stopiteration_passtrough(): + out = [] + + @hookimpl(wrapper=True) + def wrap(): + out.append("wrap") + try: + yield + finally: + out.append("wrap done") + + @hookimpl + def stop(): + out.append("stop") + raise StopIteration + + with pytest.raises(StopIteration): + try: + MC([stop, wrap], {}) + finally: + out.append("finally") + + assert out == ["wrap", "stop", "wrap done", "finally"] + + def test_suppress_inner_wrapper_teardown_exc() -> None: out = []