Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move output widget to nbclient #621

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -233,7 +233,7 @@ def get_data_files():
'async_generator',
'jupyter_server>=0.3.0',
'jupyter_client>=6.1.3',
'nbclient>=0.2.0',
'nbclient==0.4.0a1',
'nbconvert==6.0.0a3',
'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
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