Skip to content

Commit 6d83333

Browse files
fix #544: 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.
1 parent e7cd449 commit 6d83333

File tree

1 file changed

+74
-107
lines changed

1 file changed

+74
-107
lines changed

src/pluggy/_callers.py

Lines changed: 74 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66

77
from collections.abc import Generator
88
from collections.abc import Mapping
9-
from collections.abc import Sequence
109
from typing import cast
1110
from typing import NoReturn
12-
from typing import Union
11+
from typing import Sequence
1312
import warnings
1413

1514
from ._hooks import HookImpl
@@ -20,10 +19,33 @@
2019

2120
# Need to distinguish between old- and new-style hook wrappers.
2221
# Wrapping with a tuple is the fastest type-safe way I found to do it.
23-
Teardown = Union[
24-
tuple[Generator[None, Result[object], None], HookImpl],
25-
Generator[None, object, object],
26-
]
22+
Teardown = Generator[None, object, object]
23+
24+
25+
def run_legacy_hookwrapper(
26+
hook_impl: HookImpl, hook_name: str, args: Sequence[object]
27+
) -> Teardown:
28+
teardown: Teardown = cast(Teardown, hook_impl.function(*args))
29+
try:
30+
next(teardown)
31+
except StopIteration:
32+
_raise_wrapfail(teardown, "did not yield")
33+
try:
34+
res = yield
35+
result = Result(res, None)
36+
except BaseException as exc:
37+
result = Result(None, exc)
38+
try:
39+
teardown.send(result)
40+
except StopIteration:
41+
return result.get_result()
42+
except BaseException as e:
43+
_warn_teardown_exception(hook_name, hook_impl, e)
44+
raise
45+
else:
46+
_raise_wrapfail(teardown, "has second yield")
47+
finally:
48+
teardown.close()
2749

2850

2951
def _raise_wrapfail(
@@ -45,7 +67,7 @@ def _warn_teardown_exception(
4567
msg += f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n"
4668
msg += f"{type(e).__name__}: {e}\n"
4769
msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501
48-
warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=5)
70+
warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=6)
4971

5072

5173
def _multicall(
@@ -62,7 +84,6 @@ def _multicall(
6284
__tracebackhide__ = True
6385
results: list[object] = []
6486
exception = None
65-
only_new_style_wrappers = True
6687
try: # run impl and wrapper setup functions in a loop
6788
teardowns: list[Teardown] = []
6889
try:
@@ -77,16 +98,17 @@ def _multicall(
7798
)
7899

79100
if hook_impl.hookwrapper:
80-
only_new_style_wrappers = False
81101
try:
82102
# If this cast is not valid, a type error is raised below,
83103
# which is the desired response.
84-
res = hook_impl.function(*args)
85-
wrapper_gen = cast(Generator[None, Result[object], None], res)
86-
next(wrapper_gen) # first yield
87-
teardowns.append((wrapper_gen, hook_impl))
104+
function_gen = run_legacy_hookwrapper(
105+
hook_impl, hook_name, args
106+
)
107+
108+
next(function_gen) # first yield
109+
teardowns.append(function_gen)
88110
except StopIteration:
89-
_raise_wrapfail(wrapper_gen, "did not yield")
111+
_raise_wrapfail(function_gen, "did not yield")
90112
elif hook_impl.wrapper:
91113
try:
92114
# If this cast is not valid, a type error is raised below,
@@ -106,99 +128,44 @@ def _multicall(
106128
except BaseException as exc:
107129
exception = exc
108130
finally:
109-
# Fast path - only new-style wrappers, no Result.
110-
if only_new_style_wrappers:
111-
if firstresult: # first result hooks return a single value
112-
result = results[0] if results else None
113-
else:
114-
result = results
115-
116-
# run all wrapper post-yield blocks
117-
for teardown in reversed(teardowns):
118-
try:
119-
if exception is not None:
120-
try:
121-
teardown.throw(exception) # type: ignore[union-attr]
122-
except RuntimeError as re:
123-
# StopIteration from generator causes RuntimeError
124-
# even for coroutine usage - see #544
125-
if (
126-
isinstance(exception, StopIteration)
127-
and re.__cause__ is exception
128-
):
129-
teardown.close() # type: ignore[union-attr]
130-
continue
131-
else:
132-
raise
133-
else:
134-
teardown.send(result) # type: ignore[union-attr]
135-
# Following is unreachable for a well behaved hook wrapper.
136-
# Try to force finalizers otherwise postponed till GC action.
137-
# Note: close() may raise if generator handles GeneratorExit.
138-
teardown.close() # type: ignore[union-attr]
139-
except StopIteration as si:
140-
result = si.value
141-
exception = None
142-
continue
143-
except BaseException as e:
144-
exception = e
145-
continue
146-
_raise_wrapfail(teardown, "has second yield") # type: ignore[arg-type]
147-
148-
if exception is not None:
149-
raise exception
150-
else:
151-
return result
152-
153-
# Slow path - need to support old-style wrappers.
131+
if firstresult: # first result hooks return a single value
132+
result = results[0] if results else None
154133
else:
155-
if firstresult: # first result hooks return a single value
156-
outcome: Result[object | list[object]] = Result(
157-
results[0] if results else None, exception
158-
)
159-
else:
160-
outcome = Result(results, exception)
161-
162-
# run all wrapper post-yield blocks
163-
for teardown in reversed(teardowns):
164-
if isinstance(teardown, tuple):
165-
try:
166-
teardown[0].send(outcome)
167-
except StopIteration:
168-
pass
169-
except BaseException as e:
170-
_warn_teardown_exception(hook_name, teardown[1], e)
171-
raise
172-
else:
173-
_raise_wrapfail(teardown[0], "has second yield")
174-
else:
134+
result = results
135+
136+
# run all wrapper post-yield blocks
137+
for teardown in reversed(teardowns):
138+
try:
139+
if exception is not None:
175140
try:
176-
if outcome._exception is not None:
177-
try:
178-
teardown.throw(outcome._exception)
179-
except RuntimeError as re:
180-
# StopIteration from generator causes RuntimeError
181-
# even for coroutine usage - see #544
182-
if (
183-
isinstance(outcome._exception, StopIteration)
184-
and re.__cause__ is outcome._exception
185-
):
186-
teardown.close()
187-
continue
188-
else:
189-
raise
141+
teardown.throw(exception)
142+
except RuntimeError as re:
143+
# StopIteration from generator causes RuntimeError
144+
# even for coroutine usage - see #544
145+
if (
146+
isinstance(exception, StopIteration)
147+
and re.__cause__ is exception
148+
):
149+
teardown.close()
150+
continue
190151
else:
191-
teardown.send(outcome._result)
192-
# Following is unreachable for a well behaved hook wrapper.
193-
# Try to force finalizers otherwise postponed till GC action.
194-
# Note: close() may raise if generator handles GeneratorExit.
195-
teardown.close()
196-
except StopIteration as si:
197-
outcome.force_result(si.value)
198-
continue
199-
except BaseException as e:
200-
outcome.force_exception(e)
201-
continue
202-
_raise_wrapfail(teardown, "has second yield")
203-
204-
return outcome.get_result()
152+
raise
153+
else:
154+
teardown.send(result)
155+
# Following is unreachable for a well behaved hook wrapper.
156+
# Try to force finalizers otherwise postponed till GC action.
157+
# Note: close() may raise if generator handles GeneratorExit.
158+
teardown.close()
159+
except StopIteration as si:
160+
result = si.value
161+
exception = None
162+
continue
163+
except BaseException as e:
164+
exception = e
165+
continue
166+
_raise_wrapfail(teardown, "has second yield")
167+
168+
if exception is not None:
169+
raise exception
170+
else:
171+
return result

0 commit comments

Comments
 (0)