Skip to content

Commit

Permalink
refactor: move output widget to nbclient
Browse files Browse the repository at this point in the history
  • Loading branch information
maartenbreddels committed May 26, 2020
1 parent 0d5d6f2 commit 1b88abf
Show file tree
Hide file tree
Showing 4 changed files with 3 additions and 126 deletions.
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
whereis python
python --version
pip install --upgrade --no-deps --force-reinstall git+https://github.com/maartenbreddels/nbclient.git@feat_mimic_output_widget
python -m pip install --ignore-installed ".[test]"
cd tests/test_template; pip install .; cd ../../;
Expand Down
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ before_install:
- conda info -a
- conda create -q -n test-environment -c conda-forge python=$PYTHON_VERSION jupyterlab_pygments==0.1.0 pytest-cov nodejs flake8 ipywidgets matplotlib xeus-cling
- source activate test-environment
- pip install --upgrade --no-deps --force-reinstall git+https://github.com/maartenbreddels/nbclient.git@feat_mimic_output_widget
install:
- pip install --ignore-installed ".[test]"
- cd tests/test_template; pip install .; cd ../../;
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ def get_data_files():
'async_generator',
'jupyter_server>=0.3.0',
'jupyter_client>=6.1.3',
'nbclient>=0.2.0',
'nbclient>=0.3.0',
'nbconvert==6.0.0a1',
'jupyterlab_pygments>=0.1.0,<0.2',
'pygments>=2.4.1,<3' # Explicitly requiring pygments which is a second-order dependency.
Expand Down
125 changes: 0 additions & 125 deletions voila/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@
# #
# The full license is in the file LICENSE, distributed with this software. #
#############################################################################
import collections
import logging

from nbconvert.preprocessors import ClearOutputPreprocessor
from nbclient.exceptions import CellExecutionError
from nbclient import NotebookClient
from nbformat.v4 import output_from_msg

from traitlets import Unicode
from ipykernel.jsonutil import json_clean


def strip_code_cell_warnings(cell):
Expand All @@ -38,80 +35,6 @@ def should_strip_error(config):
return 'Voila' not in config or 'log_level' not in config['Voila'] or config['Voila']['log_level'] != logging.DEBUG


class OutputWidget:
"""This class mimics a front end output widget"""
def __init__(self, comm_id, state, kernel_client, executor):
self.comm_id = comm_id
self.state = state
self.kernel_client = kernel_client
self.executor = executor
self.topic = ('comm-%s' % self.comm_id).encode('ascii')
self.outputs = self.state['outputs']
self.clear_before_next_output = False

def clear_output(self, outs, msg, cell_index):
self.parent_header = msg['parent_header']
content = msg['content']
if content.get('wait'):
self.clear_before_next_output = True
else:
self.outputs = []
# sync back the state to the kernel
self.sync_state()
if hasattr(self.executor, 'widget_state'):
# sync the state to the nbconvert state as well, since that is used for testing
self.executor.widget_state[self.comm_id]['outputs'] = self.outputs

def sync_state(self):
state = {'outputs': self.outputs}
msg = {'method': 'update', 'state': state, 'buffer_paths': []}
self.send(msg)

def _publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys):
"""Helper for sending a comm message on IOPub"""
data = {} if data is None else data
metadata = {} if metadata is None else metadata
content = json_clean(dict(data=data, comm_id=self.comm_id, **keys))
msg = self.kernel_client.session.msg(msg_type, content=content, parent=self.parent_header, metadata=metadata)
self.kernel_client.shell_channel.send(msg)

def send(self, data=None, metadata=None, buffers=None):
self._publish_msg('comm_msg', data=data, metadata=metadata, buffers=buffers)

def output(self, outs, msg, display_id, cell_index):
if self.clear_before_next_output:
self.outputs = []
self.clear_before_next_output = False
self.parent_header = msg['parent_header']
output = output_from_msg(msg)

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
self.executor.widget_state[self.comm_id]['outputs'] = self.outputs

def set_state(self, state):
if 'msg_id' in state:
msg_id = state.get('msg_id')
if msg_id:
self.executor.register_output_hook(msg_id, self)
self.msg_id = msg_id
else:
self.executor.remove_output_hook(self.msg_id, self)
self.msg_id = msg_id


class VoilaExecutor(NotebookClient):
"""Execute, but respect the output widget behaviour"""
cell_error_instruction = Unicode(
Expand All @@ -132,8 +55,6 @@ class VoilaExecutor(NotebookClient):

def __init__(self, nb, km=None, **kwargs):
super(VoilaExecutor, self).__init__(nb, km=km, **kwargs)
self.output_hook_stack = collections.defaultdict(list) # maps to list of hooks, where the last is used
self.output_objects = {}

def execute(self, nb, resources, km=None):
try:
Expand Down Expand Up @@ -166,52 +87,6 @@ async def execute_cell(self, cell, resources, cell_index, store_history=True):

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 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(VoilaExecutor, self).output(outs, msg, display_id, cell_index)

def handle_comm_msg(self, outs, msg, cell_index):
super(VoilaExecutor, self).handle_comm_msg(outs, msg, cell_index)
self.log.debug('comm msg: %r', msg)
if msg['msg_type'] == 'comm_open' and msg['content'].get('target_name') == 'jupyter.widget':
content = msg['content']
data = content['data']
state = data['state']
comm_id = msg['content']['comm_id']
if state['_model_module'] == '@jupyter-widgets/output' and state['_model_name'] == 'OutputModel':
self.output_objects[comm_id] = OutputWidget(comm_id, state, self.kc, self)
elif msg['msg_type'] == 'comm_msg':
content = msg['content']
data = content['data']
if 'state' in data:
state = data['state']
comm_id = msg['content']['comm_id']
if comm_id in self.output_objects:
self.output_objects[comm_id].set_state(state)

def clear_output(self, outs, msg, cell_index):
parent_msg_id = msg['parent_header'].get('msg_id')
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(VoilaExecutor, self).clear_output(outs, msg, cell_index)

def strip_notebook_errors(self, nb):
"""Strip error messages and traceback from a Notebook."""
cells = nb['cells']
Expand Down

0 comments on commit 1b88abf

Please sign in to comment.