|
19 | 19 | import typing as t
|
20 | 20 |
|
21 | 21 | from traitlets.config.configurable import LoggingConfigurable
|
22 |
| -from traitlets import List, Unicode, Bool, Enum, Any, Type, Dict, Integer, default |
| 22 | +from traitlets import List, Unicode, Bool, Enum, Any, Type, Dict, Integer, default, Callable |
23 | 23 |
|
24 | 24 | from nbformat import NotebookNode
|
25 | 25 | from nbformat.v4 import output_from_msg
|
@@ -285,6 +285,16 @@ def _kernel_manager_class_default(self) -> KernelManager:
|
285 | 285 | """,
|
286 | 286 | ).tag(config=True)
|
287 | 287 |
|
| 288 | + parse_md_expressions: t.Optional[t.Callable[[str], t.List[str]]] = Callable( |
| 289 | + None, |
| 290 | + allow_none=True, |
| 291 | + help=dedent( |
| 292 | + """ |
| 293 | + A function to extract expression variables from a Markdown cell. |
| 294 | + """ |
| 295 | + ) |
| 296 | + ) |
| 297 | + |
288 | 298 | resources: t.Dict = Dict(
|
289 | 299 | help=dedent(
|
290 | 300 | """
|
@@ -804,6 +814,14 @@ async def async_execute_cell(
|
804 | 814 | The cell which was just processed.
|
805 | 815 | """
|
806 | 816 | assert self.kc is not None
|
| 817 | + |
| 818 | + if self.parse_md_expressions and cell.cell_type == 'markdown': |
| 819 | + expressions = self.parse_md_expressions(cell.source) |
| 820 | + if expressions: |
| 821 | + if not "attachments" in cell: |
| 822 | + cell.attachments = {} |
| 823 | + cell.attachments.update(await self.async_execute_expressions(cell, cell_index, expressions)) |
| 824 | + |
807 | 825 | if cell.cell_type != 'code' or not cell.source.strip():
|
808 | 826 | self.log.debug("Skipping non-executing cell %s", cell_index)
|
809 | 827 | return cell
|
@@ -1033,6 +1051,105 @@ def handle_comm_msg(
|
1033 | 1051 | if comm_id in self.comm_objects:
|
1034 | 1052 | self.comm_objects[comm_id].handle_msg(msg)
|
1035 | 1053 |
|
| 1054 | + async def async_execute_expressions(self, cell, cell_index: int, expressions: t.List[str]) -> t.Dict[str, Any]: |
| 1055 | + user_expressions = {f"md-expr-{i}": expr for i, expr in enumerate(expressions)} |
| 1056 | + print(user_expressions) |
| 1057 | + parent_msg_id = await ensure_async( |
| 1058 | + self.kc.execute( |
| 1059 | + '', |
| 1060 | + silent=True, |
| 1061 | + user_expressions=user_expressions, |
| 1062 | + ) |
| 1063 | + ) |
| 1064 | + task_poll_kernel_alive = asyncio.ensure_future( |
| 1065 | + self._async_poll_kernel_alive() |
| 1066 | + ) |
| 1067 | + task_poll_expr_msg = asyncio.ensure_future( |
| 1068 | + self._async_poll_expr_msg(parent_msg_id, cell, cell_index) |
| 1069 | + ) |
| 1070 | + exec_timeout = None |
| 1071 | + self.task_poll_for_reply = asyncio.ensure_future( |
| 1072 | + self._async_poll_for_expr_reply( |
| 1073 | + parent_msg_id, cell, exec_timeout, task_poll_expr_msg, task_poll_kernel_alive |
| 1074 | + ) |
| 1075 | + ) |
| 1076 | + try: |
| 1077 | + exec_reply = await self.task_poll_for_reply |
| 1078 | + except asyncio.CancelledError: |
| 1079 | + # can only be cancelled by task_poll_kernel_alive when the kernel is dead |
| 1080 | + task_poll_expr_msg.cancel() |
| 1081 | + raise DeadKernelError("Kernel died") |
| 1082 | + except Exception as e: |
| 1083 | + # Best effort to cancel request if it hasn't been resolved |
| 1084 | + try: |
| 1085 | + # Check if the task_poll_output is doing the raising for us |
| 1086 | + if not isinstance(e, CellControlSignal): |
| 1087 | + task_poll_expr_msg.cancel() |
| 1088 | + finally: |
| 1089 | + raise |
| 1090 | + |
| 1091 | + async def _async_poll_for_expr_reply( |
| 1092 | + self, |
| 1093 | + msg_id: str, |
| 1094 | + cell: NotebookNode, |
| 1095 | + timeout: t.Optional[int], |
| 1096 | + task_poll_output_msg: asyncio.Future, |
| 1097 | + task_poll_kernel_alive: asyncio.Future) -> t.Dict: |
| 1098 | + |
| 1099 | + assert self.kc is not None |
| 1100 | + new_timeout: t.Optional[float] = None |
| 1101 | + if timeout is not None: |
| 1102 | + deadline = monotonic() + timeout |
| 1103 | + new_timeout = float(timeout) |
| 1104 | + while True: |
| 1105 | + try: |
| 1106 | + msg = await ensure_async(self.kc.shell_channel.get_msg(timeout=new_timeout)) |
| 1107 | + if msg['parent_header'].get('msg_id') == msg_id: |
| 1108 | + try: |
| 1109 | + await asyncio.wait_for(task_poll_output_msg, self.iopub_timeout) |
| 1110 | + except (asyncio.TimeoutError, Empty): |
| 1111 | + if self.raise_on_iopub_timeout: |
| 1112 | + task_poll_kernel_alive.cancel() |
| 1113 | + raise CellTimeoutError.error_from_timeout_and_cell( |
| 1114 | + "Timeout waiting for IOPub output", self.iopub_timeout, cell |
| 1115 | + ) |
| 1116 | + else: |
| 1117 | + self.log.warning("Timeout waiting for IOPub output") |
| 1118 | + task_poll_kernel_alive.cancel() |
| 1119 | + return msg |
| 1120 | + else: |
| 1121 | + if new_timeout is not None: |
| 1122 | + new_timeout = max(0, deadline - monotonic()) |
| 1123 | + except Empty: |
| 1124 | + # received no message, check if kernel is still alive |
| 1125 | + assert timeout is not None |
| 1126 | + task_poll_kernel_alive.cancel() |
| 1127 | + await self._async_check_alive() |
| 1128 | + await self._async_handle_timeout(timeout, cell) |
| 1129 | + |
| 1130 | + async def _async_poll_expr_msg( |
| 1131 | + self, |
| 1132 | + parent_msg_id: str, |
| 1133 | + cell: NotebookNode, |
| 1134 | + cell_index: int) -> None: |
| 1135 | + |
| 1136 | + assert self.kc is not None |
| 1137 | + while True: |
| 1138 | + msg = await ensure_async(self.kc.iopub_channel.get_msg(timeout=None)) |
| 1139 | + if msg['parent_header'].get('msg_id') == parent_msg_id: |
| 1140 | + try: |
| 1141 | + # Will raise CellExecutionComplete when completed |
| 1142 | + # self.process_message(msg, cell, cell_index) |
| 1143 | + print(msg) |
| 1144 | + msg_type = msg['msg_type'] |
| 1145 | + if msg_type == 'status': |
| 1146 | + if msg['content']['execution_state'] == 'idle': |
| 1147 | + raise CellExecutionComplete() |
| 1148 | + # elif msg_type != 'execute_input': |
| 1149 | + # raise ValueError(msg) |
| 1150 | + except CellExecutionComplete: |
| 1151 | + return |
| 1152 | + |
1036 | 1153 | def _serialize_widget_state(self, state: t.Dict) -> t.Dict[str, t.Any]:
|
1037 | 1154 | """Serialize a widget state, following format in @jupyter-widgets/schema."""
|
1038 | 1155 | return {
|
|
0 commit comments