Skip to content

Commit

Permalink
fix: support nested output widgets in voila executor (#358)
Browse files Browse the repository at this point in the history
* fix: support nested output widgets in voila executor

Fixes #355
  • Loading branch information
maartenbreddels authored Aug 23, 2019
1 parent 0efb3e9 commit 09b0463
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 53 deletions.
196 changes: 151 additions & 45 deletions tests/notebooks/output.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "4d6af5172a774f7ebfdfaf36cce893c5",
"model_id": "d314a6ef74d947f3a2149bdf9b8b57a3",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -54,7 +54,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "1993ee53d793450a9a4d830c700496b5",
"model_id": "aaf673ac9c774aaba4f751db2f3dd6c5",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -100,7 +100,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "22a925976945430b8ddbb0b9b754734b",
"model_id": "2955dc9c531c4c6b80086da240d0df13",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -147,7 +147,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "3a32b286c0464dbeb3819213a3bf6133",
"model_id": "bc3d9af2591e4a52af73921f46d79efa",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -193,7 +193,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "9faaadcde2e94306b7187ffc1587cd24",
"model_id": "4fa2d1a41bd64017a20e358526ad9cf3",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -222,6 +222,70 @@
" display(\"hello world\") # this is not a stream but plain text\n",
"clear_output()"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "7134e81fdb364a738c1e58b26ec0d008",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Output()"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import ipywidgets as widgets\n",
"output_outer = widgets.Output()\n",
"output_inner = widgets.Output()\n",
"output_inner"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "a32671b19b814cf5bd964c36368f9f79",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Output()"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"output_outer"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"with output_inner:\n",
" print('in inner')\n",
" with output_outer:\n",
" print('in outer')\n",
" print('also in inner')"
]
}
],
"metadata": {
Expand All @@ -238,110 +302,152 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.4"
"version": "3.6.7"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {
"0c97c6f319e6494e918df877e90f1ede": {
"025929abe8a143a08ad23de9e99c610f": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.2.0",
"model_name": "LayoutModel",
"state": {}
},
"106de0ded502439c873de5449248b00c": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.2.0",
"model_name": "LayoutModel",
"state": {}
},
"1b9529b98aaf40ccbbf38e178796be88": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.2.0",
"model_name": "LayoutModel",
"state": {}
},
"22592f3cb7674cb79cc60def5e8bc060": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_module_version": "1.2.0",
"model_name": "LayoutModel",
"state": {}
},
"1993ee53d793450a9a4d830c700496b5": {
"2955dc9c531c4c6b80086da240d0df13": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_5584875f1aa94595852160dada91122a",
"layout": "IPY_MODEL_1b9529b98aaf40ccbbf38e178796be88",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": "in output2\n"
"text": "world\n"
}
]
}
},
"4fa2d1a41bd64017a20e358526ad9cf3": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_6490daaa1d2e42a0aef909e7b8c8eff4",
"outputs": [
{
"data": {
"text/plain": "'hello world'"
},
"metadata": {},
"output_type": "display_data"
}
]
}
},
"22a925976945430b8ddbb0b9b754734b": {
"6490daaa1d2e42a0aef909e7b8c8eff4": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.2.0",
"model_name": "LayoutModel",
"state": {}
},
"7134e81fdb364a738c1e58b26ec0d008": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_a3204e1e6a6649c2802861b9f389314e",
"layout": "IPY_MODEL_025929abe8a143a08ad23de9e99c610f",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": "world\n"
"text": "in inner\nalso in inner\n"
}
]
}
},
"3a32b286c0464dbeb3819213a3bf6133": {
"804b6628ca0a48dfbad930615626b1fb": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.2.0",
"model_name": "LayoutModel",
"state": {}
},
"a32671b19b814cf5bd964c36368f9f79": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_561942f986f34d4ca3f7bdbe717e9289"
"layout": "IPY_MODEL_c843c22ff72e4983984ca4d62ce68e2b",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": "in outer\n"
}
]
}
},
"4d6af5172a774f7ebfdfaf36cce893c5": {
"aaf673ac9c774aaba4f751db2f3dd6c5": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_9357eb7d15c64c7d9e007a2b5ab9cef0",
"layout": "IPY_MODEL_106de0ded502439c873de5449248b00c",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": "in output\n"
"text": "in output2\n"
}
]
}
},
"5584875f1aa94595852160dada91122a": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"561942f986f34d4ca3f7bdbe717e9289": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
"bc3d9af2591e4a52af73921f46d79efa": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_22592f3cb7674cb79cc60def5e8bc060"
}
},
"9357eb7d15c64c7d9e007a2b5ab9cef0": {
"c843c22ff72e4983984ca4d62ce68e2b": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_module_version": "1.2.0",
"model_name": "LayoutModel",
"state": {}
},
"9faaadcde2e94306b7187ffc1587cd24": {
"d314a6ef74d947f3a2149bdf9b8b57a3": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_0c97c6f319e6494e918df877e90f1ede",
"layout": "IPY_MODEL_804b6628ca0a48dfbad930615626b1fb",
"outputs": [
{
"data": {
"text/plain": "'hello world'"
},
"metadata": {},
"output_type": "display_data"
"name": "stdout",
"output_type": "stream",
"text": "in output\n"
}
]
}
},
"a3204e1e6a6649c2802861b9f389314e": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
}
},
"version_major": 2,
Expand Down
40 changes: 32 additions & 8 deletions voila/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# #
# The full license is in the file LICENSE, distributed with this software. #
#############################################################################
import collections

from nbconvert.preprocessors import ClearOutputPreprocessor
from nbconvert.preprocessors.execute import CellExecutionError, ExecutePreprocessor
Expand Down Expand Up @@ -63,7 +64,17 @@ def output(self, outs, msg, display_id, cell_index):
else:
data = content['data']
output = {"output_type": "display_data", "data": data, "metadata": {}}
self.outputs.append(output)
if self.outputs:
# try to coalesce/merge output text
last_output = self.outputs[-1]
if (last_output['output_type'] == 'stream' and
output['output_type'] == 'stream' and
last_output['name'] == output['name']):
last_output['text'] += output['text']
else:
self.outputs.append(output)
else:
self.outputs.append(output)
self.sync_state()
if hasattr(self.executor, 'widget_state'):
# sync the state to the nbconvert state as well, since that is used for testing
Expand All @@ -73,17 +84,17 @@ def set_state(self, state):
if 'msg_id' in state:
msg_id = state.get('msg_id')
if msg_id:
self.executor.output_hook[msg_id] = self
self.executor.register_output_hook(msg_id, self)
self.msg_id = msg_id
else:
del self.executor.output_hook[self.msg_id]
self.executor.remove_output_hook(self.msg_id, self)
self.msg_id = msg_id


class VoilaExecutePreprocessor(ExecutePreprocessor):
"""Execute, but respect the output widget behaviour"""
def preprocess(self, nb, resources, km=None):
self.output_hook = {}
self.output_hook_stack = collections.defaultdict(list) # maps to list of hooks, where the last is used
self.output_objects = {}
try:
result = super(VoilaExecutePreprocessor, self).preprocess(nb, resources=resources, km=km)
Expand All @@ -92,10 +103,22 @@ def preprocess(self, nb, resources, km=None):
result = (nb, resources)
return result

def register_output_hook(self, msg_id, hook):
# mimics
# https://jupyterlab.github.io/jupyterlab/services/interfaces/kernel.ikernelconnection.html#registermessagehook
self.output_hook_stack[msg_id].append(hook)

def remove_output_hook(self, msg_id, hook):
# mimics
# https://jupyterlab.github.io/jupyterlab/services/interfaces/kernel.ikernelconnection.html#removemessagehook
removed_hook = self.output_hook_stack[msg_id].pop()
assert removed_hook == hook

def output(self, outs, msg, display_id, cell_index):
parent_msg_id = msg['parent_header'].get('msg_id')
if parent_msg_id in self.output_hook:
self.output_hook[parent_msg_id].output(outs, msg, display_id, cell_index)
if self.output_hook_stack[parent_msg_id]:
hook = self.output_hook_stack[parent_msg_id][-1]
hook.output(outs, msg, display_id, cell_index)
return
super(VoilaExecutePreprocessor, self).output(outs, msg, display_id, cell_index)

Expand All @@ -120,8 +143,9 @@ def handle_comm_msg(self, outs, msg, cell_index):

def clear_output(self, outs, msg, cell_index):
parent_msg_id = msg['parent_header'].get('msg_id')
if parent_msg_id in self.output_hook:
self.output_hook[parent_msg_id].clear_output(outs, msg, cell_index)
if self.output_hook_stack[parent_msg_id]:
hook = self.output_hook_stack[parent_msg_id][-1]
hook.clear_output(outs, msg, cell_index)
return
super(VoilaExecutePreprocessor, self).clear_output(outs, msg, cell_index)

Expand Down

0 comments on commit 09b0463

Please sign in to comment.