|
7 | 7 | import time
|
8 | 8 | from asyncio import (
|
9 | 9 | AbstractEventLoop,
|
10 |
| - CancelledError, |
11 | 10 | Future,
|
12 | 11 | Task,
|
13 | 12 | ensure_future,
|
|
32 | 31 | Iterator,
|
33 | 32 | List,
|
34 | 33 | Optional,
|
| 34 | + Set, |
35 | 35 | Tuple,
|
36 | 36 | Type,
|
37 | 37 | TypeVar,
|
@@ -433,7 +433,7 @@ def reset(self) -> None:
|
433 | 433 |
|
434 | 434 | self.exit_style = ""
|
435 | 435 |
|
436 |
| - self.background_tasks: List[Task[None]] = [] |
| 436 | + self._background_tasks: Set[Task[None]] = set() |
437 | 437 |
|
438 | 438 | self.renderer.reset()
|
439 | 439 | self.key_processor.reset()
|
@@ -1066,32 +1066,75 @@ def create_background_task(
|
1066 | 1066 | the `Application` terminates, unfinished background tasks will be
|
1067 | 1067 | cancelled.
|
1068 | 1068 |
|
1069 |
| - If asyncio had nurseries like Trio, we would create a nursery in |
1070 |
| - `Application.run_async`, and run the given coroutine in that nursery. |
| 1069 | + Given that we still support Python versions before 3.11, we can't use |
| 1070 | + task groups (and exception groups), because of that, these background |
| 1071 | + tasks are not allowed to raise exceptions. If they do, we'll call the |
| 1072 | + default exception handler from the event loop. |
1071 | 1073 |
|
1072 |
| - Not threadsafe. |
| 1074 | + If at some point, we have Python 3.11 as the minimum supported Python |
| 1075 | + version, then we can use a `TaskGroup` (with the lifetime of |
| 1076 | + `Application.run_async()`, and run run the background tasks in there. |
| 1077 | +
|
| 1078 | + This is not threadsafe. |
1073 | 1079 | """
|
1074 | 1080 | task: asyncio.Task[None] = get_event_loop().create_task(coroutine)
|
1075 |
| - self.background_tasks.append(task) |
| 1081 | + self._background_tasks.add(task) |
| 1082 | + |
| 1083 | + task.add_done_callback(self._on_background_task_done) |
1076 | 1084 | return task
|
1077 | 1085 |
|
| 1086 | + def _on_background_task_done(self, task: "asyncio.Task[None]") -> None: |
| 1087 | + """ |
| 1088 | + Called when a background task completes. Remove it from |
| 1089 | + `_background_tasks`, and handle exceptions if any. |
| 1090 | + """ |
| 1091 | + self._background_tasks.discard(task) |
| 1092 | + |
| 1093 | + if task.cancelled(): |
| 1094 | + return |
| 1095 | + |
| 1096 | + exc = task.exception() |
| 1097 | + if exc is not None: |
| 1098 | + get_event_loop().call_exception_handler( |
| 1099 | + { |
| 1100 | + "message": f"prompt_toolkit.Application background task {task!r} " |
| 1101 | + "raised an unexpected exception.", |
| 1102 | + "exception": exc, |
| 1103 | + "task": task, |
| 1104 | + } |
| 1105 | + ) |
| 1106 | + |
1078 | 1107 | async def cancel_and_wait_for_background_tasks(self) -> None:
|
1079 | 1108 | """
|
1080 |
| - Cancel all background tasks, and wait for the cancellation to be done. |
| 1109 | + Cancel all background tasks, and wait for the cancellation to complete. |
1081 | 1110 | If any of the background tasks raised an exception, this will also
|
1082 | 1111 | propagate the exception.
|
1083 | 1112 |
|
1084 | 1113 | (If we had nurseries like Trio, this would be the `__aexit__` of a
|
1085 | 1114 | nursery.)
|
1086 | 1115 | """
|
1087 |
| - for task in self.background_tasks: |
| 1116 | + for task in self._background_tasks: |
1088 | 1117 | task.cancel()
|
1089 | 1118 |
|
1090 |
| - for task in self.background_tasks: |
1091 |
| - try: |
1092 |
| - await task |
1093 |
| - except CancelledError: |
1094 |
| - pass |
| 1119 | + # Wait until the cancellation of the background tasks completes. |
| 1120 | + # `asyncio.wait()` does not propagate exceptions raised within any of |
| 1121 | + # these tasks, which is what we want. Otherwise, we can't distinguish |
| 1122 | + # between a `CancelledError` raised in this task because it got |
| 1123 | + # cancelled, and a `CancelledError` raised on this `await` checkpoint, |
| 1124 | + # because *we* got cancelled during the teardown of the application. |
| 1125 | + # (If we get cancelled here, then it's important to not suppress the |
| 1126 | + # `CancelledError`, and have it propagate.) |
| 1127 | + # NOTE: Currently, if we get cancelled at this point then we can't wait |
| 1128 | + # for the cancellation to complete (in the future, we should be |
| 1129 | + # using anyio or Python's 3.11 TaskGroup.) |
| 1130 | + # Also, if we had exception groups, we could propagate an |
| 1131 | + # `ExceptionGroup` if something went wrong here. Right now, we |
| 1132 | + # don't propagate exceptions, but have them printed in |
| 1133 | + # `_on_background_task_done`. |
| 1134 | + if len(self._background_tasks) > 0: |
| 1135 | + await asyncio.wait( |
| 1136 | + self._background_tasks, timeout=None, return_when=asyncio.ALL_COMPLETED |
| 1137 | + ) |
1095 | 1138 |
|
1096 | 1139 | async def _poll_output_size(self) -> None:
|
1097 | 1140 | """
|
|
0 commit comments