|
4 | 4 | import sys |
5 | 5 | import warnings |
6 | 6 | import weakref |
7 | | -from typing import TYPE_CHECKING, NoReturn |
| 7 | +from typing import TYPE_CHECKING, NoReturn, TypeVar |
8 | 8 |
|
9 | 9 | import attrs |
10 | 10 |
|
|
16 | 16 | ASYNCGEN_LOGGER = logging.getLogger("trio.async_generator_errors") |
17 | 17 |
|
18 | 18 | if TYPE_CHECKING: |
| 19 | + from collections.abc import Callable |
19 | 20 | from types import AsyncGeneratorType |
20 | 21 |
|
| 22 | + from typing_extensions import ParamSpec |
| 23 | + |
| 24 | + _P = ParamSpec("_P") |
| 25 | + |
21 | 26 | _WEAK_ASYNC_GEN_SET = weakref.WeakSet[AsyncGeneratorType[object, NoReturn]] |
22 | 27 | _ASYNC_GEN_SET = set[AsyncGeneratorType[object, NoReturn]] |
23 | 28 | else: |
24 | 29 | _WEAK_ASYNC_GEN_SET = weakref.WeakSet |
25 | 30 | _ASYNC_GEN_SET = set |
26 | 31 |
|
| 32 | +_R = TypeVar("_R") |
| 33 | + |
27 | 34 |
|
28 | 35 | @_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) |
52 | 42 |
|
53 | 43 |
|
54 | 44 | @attrs.define(eq=False) |
@@ -136,10 +126,29 @@ def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None: |
136 | 126 | ) |
137 | 127 | else: |
138 | 128 | # 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) |
141 | 132 | 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 | + ) |
143 | 152 |
|
144 | 153 | self.prev_hooks = sys.get_asyncgen_hooks() |
145 | 154 | sys.set_asyncgen_hooks(firstiter=firstiter, finalizer=finalizer) # type: ignore[arg-type] # Finalizer doesn't use AsyncGeneratorType |
|
0 commit comments