Skip to content

Commit 469f4a3

Browse files
committed
Try getting back user_expressions
1 parent 4758129 commit 469f4a3

File tree

4 files changed

+160
-1
lines changed

4 files changed

+160
-1
lines changed

nbclient/client.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import typing as t
2020

2121
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
2323

2424
from nbformat import NotebookNode
2525
from nbformat.v4 import output_from_msg
@@ -285,6 +285,16 @@ def _kernel_manager_class_default(self) -> KernelManager:
285285
""",
286286
).tag(config=True)
287287

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+
288298
resources: t.Dict = Dict(
289299
help=dedent(
290300
"""
@@ -804,6 +814,14 @@ async def async_execute_cell(
804814
The cell which was just processed.
805815
"""
806816
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+
807825
if cell.cell_type != 'code' or not cell.source.strip():
808826
self.log.debug("Skipping non-executing cell %s", cell_index)
809827
return cell
@@ -1033,6 +1051,105 @@ def handle_comm_msg(
10331051
if comm_id in self.comm_objects:
10341052
self.comm_objects[comm_id].handle_msg(msg)
10351053

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+
10361153
def _serialize_widget_state(self, state: t.Dict) -> t.Dict[str, t.Any]:
10371154
"""Serialize a widget state, following format in @jupyter-widgets/schema."""
10381155
return {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 1,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"a = 1\n",
10+
"b = 'c'"
11+
]
12+
},
13+
{
14+
"cell_type": "markdown",
15+
"metadata": {},
16+
"source": [
17+
"# Variables\n",
18+
"\n",
19+
"{{ a }} {{ b }}"
20+
]
21+
}
22+
],
23+
"metadata": {},
24+
"nbformat": 4,
25+
"nbformat_minor": 0
26+
}

nbclient/tests/test_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,20 @@ def notebook_resources():
241241
return {'metadata': {'path': os.path.join(current_dir, 'files')}}
242242

243243

244+
def parse_md_expressions(source: str) -> list:
245+
"""
246+
Parse markdown expressions from a string.
247+
248+
:param source: The source string to parse.
249+
:return: A list of markdown expressions.
250+
"""
251+
from markdown_it import MarkdownIt, tree
252+
from mdit_py_plugins.substitution import substitution_plugin
253+
mdit = MarkdownIt().use(substitution_plugin)
254+
tokens = tree.SyntaxTreeNode(mdit.parse(source))
255+
return [t.content for t in tokens.walk() if t.type in ['substitution_inline', 'substitution_block']]
256+
257+
244258
@pytest.mark.parametrize(
245259
["input_name", "opts"],
246260
[
@@ -262,6 +276,7 @@ def notebook_resources():
262276
("UnicodePy3.ipynb", dict(kernel_name="python")),
263277
("update-display-id.ipynb", dict(kernel_name="python")),
264278
("Check History in Memory.ipynb", dict(kernel_name="python")),
279+
("Markdown_expressions.ipynb", dict(kernel_name="python", parse_md_expressions=parse_md_expressions)),
265280
],
266281
)
267282
def test_run_all_notebooks(input_name, opts):

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ pip>=18.1
1616
wheel>=0.31.0
1717
setuptools>=38.6.0
1818
twine>=1.11.0
19+
markdown-it-py[plugins]~=1.0.0

0 commit comments

Comments
 (0)