From ad777162a09c3de06de359969943e99e437bb8f9 Mon Sep 17 00:00:00 2001 From: Rick Lupton Date: Sun, 10 Dec 2017 22:41:43 +0000 Subject: [PATCH] Save widget model state to notebook metadata Currently default values are not removed and binary buffers are not saved. (updated to conform to widget state schema) --- nbconvert/preprocessors/execute.py | 14 ++++++ .../tests/files/widget-hello-world.ipynb | 47 +++++++++++++++++++ nbconvert/preprocessors/tests/test_execute.py | 33 +++++++++++++ setup.py | 2 +- 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 nbconvert/preprocessors/tests/files/widget-hello-world.ipynb diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 205f9efc9..45587d345 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -480,6 +480,7 @@ def run_cell(self, cell, cell_index=0): continue elif msg_type.startswith('comm'): self.handle_comm_msg(outs, msg, cell_index) + self.widget_state[content['comm_id']] = content['data']['state'] continue display_id = None @@ -556,3 +557,16 @@ def executenb(nb, cwd=None, km=None, **kwargs): resources['metadata'] = {'path': cwd} ep = ExecutePreprocessor(**kwargs) return ep.preprocess(nb, resources, km=km)[0] + + +def _serialize_widget_state(state): + """Serialize a widget state, following format in @jupyter-widgets/schema. + + TODO: Does not currently split binary buffers or remove default values. + """ + return { + 'model_name': state['_model_name'], + 'model_module': state['_model_module'], + 'model_module_version': state.get('_model_module_version'), + 'state': state, + } diff --git a/nbconvert/preprocessors/tests/files/widget-hello-world.ipynb b/nbconvert/preprocessors/tests/files/widget-hello-world.ipynb new file mode 100644 index 000000000..2b56b0ccc --- /dev/null +++ b/nbconvert/preprocessors/tests/files/widget-hello-world.ipynb @@ -0,0 +1,47 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c3f75dd69f3e4602989aaa3711d2977d", + "version_major": 2, + "version_minor": 0 + }, + "text/html": [ + "

Failed to display Jupyter Widget of type Label.

\n", + "

\n", + " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", + " that the widgets JavaScript is still loading. If this message persists, it\n", + " likely means that the widgets JavaScript library is either not installed or\n", + " not enabled. See the Jupyter\n", + " Widgets Documentation for setup instructions.\n", + "

\n", + "

\n", + " If you're reading this message in another frontend (for example, a static\n", + " rendering on GitHub or NBViewer),\n", + " it may mean that your frontend doesn't currently support widgets.\n", + "

\n" + ], + "text/plain": [ + "Label(value='Hello World')" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import ipywidgets\n", + "ipywidgets.Label('Hello World')" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index fea201eeb..242d54193 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -57,6 +57,9 @@ def normalize_output(output): if 'text/plain' in output.get('data', {}): output['data']['text/plain'] = \ re.sub(addr_pat, '', output['data']['text/plain']) + if 'application/vnd.jupyter.widget-view+json' in output.get('data', {}): + output['data']['application/vnd.jupyter.widget-view+json'] \ + ['model_id'] = '' for key, value in output.get('data', {}).items(): if isinstance(value, string_types): output['data'][key] = _normalize_base64(value) @@ -305,3 +308,33 @@ def test_execute_function(self): original = copy.deepcopy(input_nb) executed = executenb(original, os.path.dirname(filename)) self.assert_notebooks_equal(original, executed) + + def test_widgets(self): + """Runs a test notebook with widgets and checks the widget state is saved.""" + input_file = os.path.join(current_dir, 'files', 'widget-hello-world.ipynb') + opts = dict(kernel_name="python") + res = self.build_resources() + res['metadata']['path'] = os.path.dirname(input_file) + input_nb, output_nb = self.run_notebook(input_file, opts, res) + + output_data = [ + output.get('data', {}) + for cell in output_nb['cells'] + for output in cell['outputs'] + ] + + model_ids = [ + data['application/vnd.jupyter.widget-view+json']['model_id'] + for data in output_data + if 'application/vnd.jupyter.widget-view+json' in data + ] + + wdata = output_nb['metadata']['widgets'] \ + ['application/vnd.jupyter.widget-state+json'] + for k in model_ids: + d = wdata['state'][k] + assert 'model_name' in d + assert 'model_module' in d + assert 'state' in d + assert 'version_major' in wdata + assert 'version_minor' in wdata diff --git a/setup.py b/setup.py index 252116275..eed6a741f 100644 --- a/setup.py +++ b/setup.py @@ -213,7 +213,7 @@ def run(self): jupyter_client_req = 'jupyter_client>=4.2' extra_requirements = { - 'test': ['pytest', 'pytest-cov', 'ipykernel', jupyter_client_req], +(??) 'test': ['pytest', 'pytest-cov', 'ipykernel', 'jupyter_client>=4.2'], 'serve': ['tornado>=4.0'], 'execute': [jupyter_client_req], 'docs': ['sphinx>=1.5.1',