Skip to content

Commit 2befccc

Browse files
committed
be surgicial in KI-unprotection
1 parent 4807c3b commit 2befccc

File tree

1 file changed

+36
-27
lines changed

1 file changed

+36
-27
lines changed

src/trio/_core/_asyncgens.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import warnings
66
import weakref
7-
from typing import TYPE_CHECKING, NoReturn
7+
from typing import TYPE_CHECKING, NoReturn, TypeVar
88

99
import attrs
1010

@@ -16,39 +16,29 @@
1616
ASYNCGEN_LOGGER = logging.getLogger("trio.async_generator_errors")
1717

1818
if TYPE_CHECKING:
19+
from collections.abc import Callable
1920
from types import AsyncGeneratorType
2021

22+
from typing_extensions import ParamSpec
23+
24+
_P = ParamSpec("_P")
25+
2126
_WEAK_ASYNC_GEN_SET = weakref.WeakSet[AsyncGeneratorType[object, NoReturn]]
2227
_ASYNC_GEN_SET = set[AsyncGeneratorType[object, NoReturn]]
2328
else:
2429
_WEAK_ASYNC_GEN_SET = weakref.WeakSet
2530
_ASYNC_GEN_SET = set
2631

32+
_R = TypeVar("_R")
33+
2734

2835
@_core.disable_ki_protection
29-
def _finalize_without_ki_protection(
30-
agen_name: str,
31-
agen: AsyncGeneratorType[object, NoReturn],
32-
) -> None:
33-
# Host has no finalizer. Reimplement the default
34-
# Python behavior with no hooks installed: throw in
35-
# GeneratorExit, step once, raise RuntimeError if
36-
# it doesn't exit.
37-
closer = agen.aclose()
38-
try:
39-
# If the next thing is a yield, this will raise RuntimeError
40-
# which we allow to propagate
41-
closer.send(None)
42-
except StopIteration:
43-
pass
44-
else:
45-
# If the next thing is an await, we get here. Give a nicer
46-
# error than the default "async generator ignored GeneratorExit"
47-
raise RuntimeError(
48-
f"Non-Trio async generator {agen_name!r} awaited something "
49-
"during finalization; install a finalization hook to "
50-
"support this, or wrap it in 'async with aclosing(...):'",
51-
)
36+
def _call_without_ki_protection(
37+
f: Callable[_P, _R],
38+
*args: _P.args,
39+
**kwargs: _P.kwargs,
40+
) -> _R:
41+
return f(*args, **kwargs)
5242

5343

5444
@attrs.define(eq=False)
@@ -136,10 +126,29 @@ def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
136126
)
137127
else:
138128
# Not ours -> forward to the host loop's async generator finalizer
139-
if self.prev_hooks.finalizer is not None:
140-
self.prev_hooks.finalizer(agen)
129+
finalizer = self.prev_hooks.finalizer
130+
if finalizer is not None:
131+
_call_without_ki_protection(finalizer, agen)
141132
else:
142-
_finalize_without_ki_protection(agen_name, agen)
133+
# Host has no finalizer. Reimplement the default
134+
# Python behavior with no hooks installed: throw in
135+
# GeneratorExit, step once, raise RuntimeError if
136+
# it doesn't exit.
137+
closer = agen.aclose()
138+
try:
139+
# If the next thing is a yield, this will raise RuntimeError
140+
# which we allow to propagate
141+
_call_without_ki_protection(closer.send, None)
142+
except StopIteration:
143+
pass
144+
else:
145+
# If the next thing is an await, we get here. Give a nicer
146+
# error than the default "async generator ignored GeneratorExit"
147+
raise RuntimeError(
148+
f"Non-Trio async generator {agen_name!r} awaited something "
149+
"during finalization; install a finalization hook to "
150+
"support this, or wrap it in 'async with aclosing(...):'",
151+
)
143152

144153
self.prev_hooks = sys.get_asyncgen_hooks()
145154
sys.set_asyncgen_hooks(firstiter=firstiter, finalizer=finalizer) # type: ignore[arg-type] # Finalizer doesn't use AsyncGeneratorType

0 commit comments

Comments
 (0)