From dd18105a4522431b4b06b59bba081e52f10294b4 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Wed, 24 May 2017 12:00:24 +0200 Subject: [PATCH 01/28] Fix uneven indent --- ipywidgets/widgets/widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ipywidgets/widgets/widget.py b/ipywidgets/widgets/widget.py index ac6f6bf7b8..8dbe467cfb 100644 --- a/ipywidgets/widgets/widget.py +++ b/ipywidgets/widgets/widget.py @@ -528,8 +528,8 @@ def add_traits(self, **traits): super(Widget, self).add_traits(**traits) for name, trait in traits.items(): if trait.get_metadata('sync'): - self.keys.append(name) - self.send_state(name) + self.keys.append(name) + self.send_state(name) def notify_change(self, change): """Called when a property has changed.""" From 73d0f99cb62c1e5744ee262447ac73c786fab423 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Wed, 24 May 2017 16:02:08 +0200 Subject: [PATCH 02/28] Add python embedding code Adds functions for generating embeddable HTML/javascript from the python API. --- ipywidgets/widgets/embed.py | 238 ++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 ipywidgets/widgets/embed.py diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py new file mode 100644 index 0000000000..116abca9f1 --- /dev/null +++ b/ipywidgets/widgets/embed.py @@ -0,0 +1,238 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +# +# +# Parts of this code is copied from IPyVolume (24.05.2017), under the following license: +# +# MIT License +# +# Copyright (c) 2016 Maarten Breddels +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Functions for generating embeddable HTML/javascript of a widget. +""" + +import json +from .widget import Widget, _remove_buffers + + +snippet_template = """ + +{widget_views} +""" + + +html_template = """ + + + + {title} + + +%s + + +""" % snippet_template + +widget_view_template = """""" + + +def _get_widgets_in_state(state): + for key in state.keys(): + yield Widget.widgets[key] + + +def get_recursive_state(widget, store=None, drop_defaults=False): + """Gets the embed state of a widget, and all other widgets it refers to as well""" + if store is None: + store = dict() + state = widget._get_embed_state(drop_defaults=drop_defaults) + store[widget.model_id] = state + for key in state['state'].keys(): + value = getattr(widget, key) + if isinstance(value, Widget): + get_recursive_state(value, store, drop_defaults=drop_defaults) + elif isinstance(value, (list, tuple)): + for item in value: + if isinstance(item, Widget): + get_recursive_state(item, store, drop_defaults=drop_defaults) + elif isinstance(value, dict): + for item in value.values(): + if isinstance(item, Widget): + get_recursive_state(item, store, drop_defaults=drop_defaults) + return store + + +def add_referring_widgets(states, drop_defaults=False): + """Add state of any widgets referring to widgets already in the store""" + found_new = False + for widget_id, widget in Widget.widgets.items(): # go over all widgets + #print("widget", widget, widget_id) + if widget_id not in states: + #print("check members") + widget_state = widget.get_state(drop_defaults=drop_defaults) + widget_state = _remove_buffers(widget_state)[0] + widgets_found = [] + for key, value in widget_state.items(): + value = getattr(widget, key) + if isinstance(value, Widget): + widgets_found.append(value) + elif isinstance(value, (list, tuple)): + for item in value: + if isinstance(item, Widget): + widgets_found.append(item) + elif isinstance(value, dict): + for item in value.values(): + if isinstance(item, Widget): + widgets_found.append(item) + #print("found", widgets_found) + for widgets_found in widgets_found: + if widgets_found.model_id in states: + #print("we found that we needed to add ", widget_id, widget) + states[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults) + found_new = True + return found_new + + +def dependency_state(widgets, drop_defaults, dependents=True): + """Get the state of all widgets specified, and their dependencies. + + If `dependents` is True (the default), widgets which depend on any of the + resolved widgets will be added as well. + + In the below graph, D and E are depencies of C; A and B are dependents of C; + and F is an dependent of E. That means the state will include (C, D, E) for + dependents=False, and (A, B, C, D, E, F) for dependents=True. + + A -- -- D + | -- C -- | + B -- -- + | -- E + F -- + + ---- Dependecy ----> + """ + # collect the state of all relevant widgets + if widgets is None: + widgets = Widget.widgets.values() + state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=widgets)['state'] + else: + try: + widgets[0] + except (IndexError, TypeError): + widgets = [widgets] + state = {} + for widget in widgets: + get_recursive_state(widget, state, drop_defaults=drop_defaults) + if dependents: + # it may be that other widgets refer to the collected widgets, + # such as layouts, include those as well + while add_referring_widgets(state): + pass + return state + + +def embed_data(widgets=None, expand_dependencies='full', drop_defaults=True): + """Gets data for embedding. + + Use this to get the raw data for embedding if you have special + formatting needs. + + Returns a dictionary with the following entries: + manager_state: dict of the widget manager state data + model_ids: a list of widget model IDs + """ + if expand_dependencies in ('full', 'partial'): + dependents = expand_dependencies == 'full' + state = dependency_state(widgets, drop_defaults, dependents=dependents) + widgets = tuple(_get_widgets_in_state(state)) + else: + state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=widgets)['state'] + + # Rely on ipywidget to get the default values + json_data = Widget.get_manager_state(widgets=[]) + # but plug in our own state + json_data['state'] = state + + return dict(manager_state=json_data, model_ids=[w.model_id for w in widgets]) + + +def embed_snippet(widgets=None, + expand_dependencies='full', + drop_defaults=True, + indent=2, + ): + """Return a snippet that can be embedded in an HTML file. """ + + data = embed_data(widgets, expand_dependencies, drop_defaults) + + widget_views = '\n'.join( + widget_view_template.format(**dict(model_id=model_id)) for model_id in data['model_ids'] + ) + + values = { + # TODO: Get widgets npm version automatically: + 'embed_url':'https://unpkg.com/jupyter-js-widgets@~3.0.0-alpha.0/dist/embed.js', + 'json_data': json.dumps(data['manager_state'], indent=indent), + 'widget_views': widget_views, + } + + return snippet_template.format(**values) + + +def embed_minimal_html(fp, + widgets=None, + expand_dependencies='full', + drop_defaults=False, + indent=2, + title='', + ): + """Write a minimal HTML file with widgets embedded.""" + + data = embed_data(widgets, expand_dependencies, drop_defaults) + + widget_views = '\n'.join( + widget_view_template.format(**dict(model_id=model_id)) for model_id in data['model_ids'] + ) + + values = { + # TODO: Get widgets npm version automatically: + "embed_url":"https://unpkg.com/jupyter-js-widgets@~3.0.0-alpha.0/dist/embed.js", + 'json_data': json.dumps(data['manager_state'], indent=indent), + 'widget_views': widget_views, + 'title': title, + } + + html_code = html_template.format(**values) + + # Check if fp is writable: + if hasattr(fp, 'write'): + fp.write(html_code) + else: + # Assume fp is a filename: + with open(fp, "w") as f: + f.write(html_code) From 8dcc2d5d6e84a41b9bdf18f7ec354aed7f6c3ccb Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Wed, 24 May 2017 16:55:34 +0200 Subject: [PATCH 03/28] Use view spec explicitly --- ipywidgets/widgets/embed.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py index 116abca9f1..ee1244ede4 100644 --- a/ipywidgets/widgets/embed.py +++ b/ipywidgets/widgets/embed.py @@ -55,9 +55,7 @@ """ % snippet_template widget_view_template = """""" @@ -164,7 +162,7 @@ def embed_data(widgets=None, expand_dependencies='full', drop_defaults=True): Returns a dictionary with the following entries: manager_state: dict of the widget manager state data - model_ids: a list of widget model IDs + view_specs: a list of widget view specs """ if expand_dependencies in ('full', 'partial'): dependents = expand_dependencies == 'full' @@ -178,7 +176,7 @@ def embed_data(widgets=None, expand_dependencies='full', drop_defaults=True): # but plug in our own state json_data['state'] = state - return dict(manager_state=json_data, model_ids=[w.model_id for w in widgets]) + return dict(manager_state=json_data, view_specs=[w.get_view_spec() for w in widgets]) def embed_snippet(widgets=None, @@ -191,7 +189,8 @@ def embed_snippet(widgets=None, data = embed_data(widgets, expand_dependencies, drop_defaults) widget_views = '\n'.join( - widget_view_template.format(**dict(model_id=model_id)) for model_id in data['model_ids'] + widget_view_template.format(**dict(view_spec=json.dumps(view_spec))) + for view_spec in data['view_specs'] ) values = { @@ -216,7 +215,8 @@ def embed_minimal_html(fp, data = embed_data(widgets, expand_dependencies, drop_defaults) widget_views = '\n'.join( - widget_view_template.format(**dict(model_id=model_id)) for model_id in data['model_ids'] + widget_view_template.format(**dict(view_spec=json.dumps(view_spec))) + for view_spec in data['view_specs'] ) values = { From 8b3762aba36aed4836a0c4bb88dbf53a8122f72c Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Wed, 24 May 2017 18:20:54 +0200 Subject: [PATCH 04/28] Have `embed_minimal_html` use `embed_snippet` --- ipywidgets/widgets/embed.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py index ee1244ede4..95d11cdab4 100644 --- a/ipywidgets/widgets/embed.py +++ b/ipywidgets/widgets/embed.py @@ -49,10 +49,10 @@ {title} -%s +{snippet} -""" % snippet_template +""" widget_view_template = """""" -def _get_widgets_in_state(state): - for key in state.keys(): - yield Widget.widgets[key] - - def get_recursive_state(widget, store=None, drop_defaults=False): """Gets the embed state of a widget, and all other widgets it refers to as well""" if store is None: @@ -139,10 +134,6 @@ def dependency_state(widgets, drop_defaults, dependents=True): widgets = Widget.widgets.values() state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=widgets)['state'] else: - try: - widgets[0] - except (IndexError, TypeError): - widgets = [widgets] state = {} for widget in widgets: get_recursive_state(widget, state, drop_defaults=drop_defaults) @@ -164,10 +155,14 @@ def embed_data(widgets=None, expand_dependencies='full', drop_defaults=True): manager_state: dict of the widget manager state data view_specs: a list of widget view specs """ + if widgets is not None: + try: + widgets[0] + except (IndexError, TypeError): + widgets = [widgets] if expand_dependencies in ('full', 'partial'): dependents = expand_dependencies == 'full' state = dependency_state(widgets, drop_defaults, dependents=dependents) - widgets = tuple(_get_widgets_in_state(state)) else: state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=widgets)['state'] @@ -176,7 +171,9 @@ def embed_data(widgets=None, expand_dependencies='full', drop_defaults=True): # but plug in our own state json_data['state'] = state - return dict(manager_state=json_data, view_specs=[w.get_view_spec() for w in widgets]) + view_specs = [w.get_view_spec() for w in widgets or Widget.widgets.values()] + + return dict(manager_state=json_data, view_specs=view_specs) def embed_snippet(widgets=None, From 127fd124a94d8cf406abf14ff5117e2f05a88f6c Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Wed, 24 May 2017 19:29:06 +0200 Subject: [PATCH 07/28] Don't use all widgets by default It rarely makes sense to want to export all widgets, as we don't have a good way to tell which widget views should actually be inluded (any layouts will cause widgets to appear twice, once on their own, and once in the layout). It is still able to include all by specifying `widgets=None`, and for this case we now at least only include views of DOMWidgets. --- ipywidgets/widgets/embed.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py index 4e460d961d..872d008512 100644 --- a/ipywidgets/widgets/embed.py +++ b/ipywidgets/widgets/embed.py @@ -32,6 +32,7 @@ import json from .widget import Widget, _remove_buffers +from .domwidget import DOMWidget snippet_template = """ @@ -145,7 +146,7 @@ def dependency_state(widgets, drop_defaults, dependents=True): return state -def embed_data(widgets=None, expand_dependencies='full', drop_defaults=True): +def embed_data(widgets, expand_dependencies='full', drop_defaults=True): """Gets data for embedding. Use this to get the raw data for embedding if you have special @@ -171,12 +172,15 @@ def embed_data(widgets=None, expand_dependencies='full', drop_defaults=True): # but plug in our own state json_data['state'] = state - view_specs = [w.get_view_spec() for w in widgets or Widget.widgets.values()] + if widgets is None: + widgets = [w for w in Widget.widgets.values() if isinstance(w, DOMWidget)] + + view_specs = [w.get_view_spec() for w in widgets] return dict(manager_state=json_data, view_specs=view_specs) -def embed_snippet(widgets=None, +def embed_snippet(widgets, expand_dependencies='full', drop_defaults=True, indent=2, @@ -204,13 +208,13 @@ def embed_snippet(widgets=None, return snippet_template.format(**values) -def embed_minimal_html(fp, **kwargs): +def embed_minimal_html(widgets, fp, **kwargs): """Write a minimal HTML file with widgets embedded. Accepts keyword args similar to `embed_snippet`. """ - snippet = embed_snippet(**kwargs) + snippet = embed_snippet(widgets, **kwargs) values = { 'title': 'IPyWidget export', From 0c181abe2589b550e9a8732ce816027decf34f92 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Wed, 24 May 2017 19:54:39 +0200 Subject: [PATCH 08/28] Cleanup embed code Add some useful comments, and extract some common code into a helper function. --- ipywidgets/widgets/embed.py | 68 ++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py index 872d008512..36fb214f44 100644 --- a/ipywidgets/widgets/embed.py +++ b/ipywidgets/widgets/embed.py @@ -60,54 +60,52 @@ """ -def get_recursive_state(widget, store=None, drop_defaults=False): - """Gets the embed state of a widget, and all other widgets it refers to as well""" - if store is None: - store = dict() - state = widget._get_embed_state(drop_defaults=drop_defaults) - store[widget.model_id] = state - for key in state['state'].keys(): +def _find_widget_refs_by_state(widget, state): + """Find references to other widgets in a widget's state""" + # Copy keys to allow changes to state during iteration: + keys = tuple(state.keys()) + for key in keys: value = getattr(widget, key) + # Trivial case: Direct references to other widgets: if isinstance(value, Widget): - get_recursive_state(value, store, drop_defaults=drop_defaults) + yield value + # Also check for buried references in known, JSON-able structures + # Note: This might miss references buried in more esoteric structures elif isinstance(value, (list, tuple)): for item in value: if isinstance(item, Widget): - get_recursive_state(item, store, drop_defaults=drop_defaults) + yield item elif isinstance(value, dict): for item in value.values(): if isinstance(item, Widget): - get_recursive_state(item, store, drop_defaults=drop_defaults) + yield item + + +def get_recursive_state(widget, store=None, drop_defaults=False): + """Gets the embed state of a widget, and all other widgets it refers to as well""" + if store is None: + store = dict() + state = widget._get_embed_state(drop_defaults=drop_defaults) + store[widget.model_id] = state + + # Loop over all values included in state (i.e. don't consider excluded values): + for ref in _find_widget_refs_by_state(widget, state['state']): + get_recursive_state(ref, store, drop_defaults=drop_defaults) return store -def add_referring_widgets(states, drop_defaults=False): +def add_referring_widgets(store, drop_defaults): """Add state of any widgets referring to widgets already in the store""" found_new = False for widget_id, widget in Widget.widgets.items(): # go over all widgets - #print("widget", widget, widget_id) - if widget_id not in states: - #print("check members") + if widget_id not in store: widget_state = widget.get_state(drop_defaults=drop_defaults) widget_state = _remove_buffers(widget_state)[0] - widgets_found = [] - for key, value in widget_state.items(): - value = getattr(widget, key) - if isinstance(value, Widget): - widgets_found.append(value) - elif isinstance(value, (list, tuple)): - for item in value: - if isinstance(item, Widget): - widgets_found.append(item) - elif isinstance(value, dict): - for item in value.values(): - if isinstance(item, Widget): - widgets_found.append(item) - #print("found", widgets_found) - for widgets_found in widgets_found: - if widgets_found.model_id in states: - #print("we found that we needed to add ", widget_id, widget) - states[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults) + # Loop over all references in current widget state: + for ref in _find_widget_refs_by_state(widget, widget_state): + # If the found ref is already in state, include the found reference + if ref.model_id in store: + store[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults) found_new = True return found_new @@ -137,11 +135,11 @@ def dependency_state(widgets, drop_defaults, dependents=True): else: state = {} for widget in widgets: - get_recursive_state(widget, state, drop_defaults=drop_defaults) + get_recursive_state(widget, state, drop_defaults) if dependents: # it may be that other widgets refer to the collected widgets, # such as layouts, include those as well - while add_referring_widgets(state): + while add_referring_widgets(state, drop_defaults): pass return state @@ -208,7 +206,7 @@ def embed_snippet(widgets, return snippet_template.format(**values) -def embed_minimal_html(widgets, fp, **kwargs): +def embed_minimal_html(fp, widgets, **kwargs): """Write a minimal HTML file with widgets embedded. Accepts keyword args similar to `embed_snippet`. From a3b1aba3f974ebd418b9ea42b10c6fec2d5fe513 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 29 May 2017 16:37:10 +0200 Subject: [PATCH 09/28] Avoid circular dependency recursion --- ipywidgets/widgets/embed.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py index 36fb214f44..5415e6107d 100644 --- a/ipywidgets/widgets/embed.py +++ b/ipywidgets/widgets/embed.py @@ -90,7 +90,8 @@ def get_recursive_state(widget, store=None, drop_defaults=False): # Loop over all values included in state (i.e. don't consider excluded values): for ref in _find_widget_refs_by_state(widget, state['state']): - get_recursive_state(ref, store, drop_defaults=drop_defaults) + if ref.model_id not in store: + get_recursive_state(ref, store, drop_defaults=drop_defaults) return store From 167ea505fba557a44e8b3daa51941c75b02ce6c8 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 29 May 2017 16:39:56 +0200 Subject: [PATCH 10/28] Add simple tests --- ipywidgets/widgets/tests/test_embed.py | 113 +++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 ipywidgets/widgets/tests/test_embed.py diff --git a/ipywidgets/widgets/tests/test_embed.py b/ipywidgets/widgets/tests/test_embed.py new file mode 100644 index 0000000000..4c96052973 --- /dev/null +++ b/ipywidgets/widgets/tests/test_embed.py @@ -0,0 +1,113 @@ + +import json +import os +import re +import tempfile +import shutil + +import traitlets + +from .. import IntSlider, IntText, Widget, jslink, HBox, widget_serialization +from ..embed import embed_data, embed_snippet, embed_minimal_html + +try: + from io import StringIO +except ImportError: + from StringIO import StringIO + + +class CaseWidget(Widget): + """Widget to test dependency traversal""" + + a = traitlets.Instance(Widget, allow_none=True).tag(sync=True, **widget_serialization) + b = traitlets.Instance(Widget, allow_none=True).tag(sync=True, **widget_serialization) + + _model_name = traitlets.Unicode('CaseWidgetModel').tag(sync=True) + + other = traitlets.Dict().tag(sync=True, **widget_serialization) + + + + +class TestEmbed: + + def teardown(self): + for w in tuple(Widget.widgets.values()): + w.close() + + def test_embed_data_simple(self): + w = IntText(4) + data = embed_data(widgets=w, drop_defaults=True) + + state = data['manager_state']['state'] + views = data['view_specs'] + + assert len(state) == 3 + assert len(views) == 1 + + model_names = [s['model_name'] for s in state.values()] + assert 'IntTextModel' in model_names + + def test_embed_data_two_widgets(self): + w1 = IntText(4) + w2 = IntSlider(min=0, max=100) + jslink((w1, 'value'), (w2, 'value')) + data = embed_data(widgets=[w1, w2], drop_defaults=True) + + state = data['manager_state']['state'] + views = data['view_specs'] + + assert len(state) == 7 + assert len(views) == 2 + + model_names = [s['model_name'] for s in state.values()] + assert 'IntTextModel' in model_names + assert 'IntSliderModel' in model_names + + def test_snippet(self): + w = IntText(4) + snippet = embed_snippet(widgets=w, drop_defaults=True) + + lines = snippet.splitlines() + + # Check first line with regex + re.match('', lines[0]) + + # Check simple equality on intermediate lines + assert ( + lines[1] == '' and + lines[-3] == '' + ) + + # Check state and view pass simple sanity checks: + manager_state = json.loads('\n'.join(lines[2:-4])) + state = manager_state['state'] + view = json.loads(lines[-2]) + + assert isinstance(state, dict) + assert len(state) == 3 + assert isinstance(view, dict) + + def test_minimal_html_filename(self): + w = IntText(4) + + tmpd = tempfile.mkdtemp() + + try: + output = os.path.join(tmpd, 'test.html') + embed_minimal_html(output, widgets=w, drop_defaults=True) + # Check that the file is written to the intended destination: + with open(output, 'r') as f: + content = f.read() + assert content.splitlines()[0] == '' + finally: + shutil.rmtree(tmpd) + + def test_minimal_html_filehandle(self): + w = IntText(4) + output = StringIO() + embed_minimal_html(output, widgets=w, drop_defaults=True) + content = output.getvalue() + assert content.splitlines()[0] == '' From 492ccf414f36f7f97fabc84823950c9609544639 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 29 May 2017 16:55:07 +0200 Subject: [PATCH 11/28] Simplify embed resolution Normally, the only referring widgets you would want to add are (fully resolved) links, so this commit removes the resolution of referring widgets, and instead adds a tailored function to include any such fully resolved links. --- ipywidgets/widgets/embed.py | 56 +++++++++++++++---------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py index 5415e6107d..5ca5e59a52 100644 --- a/ipywidgets/widgets/embed.py +++ b/ipywidgets/widgets/embed.py @@ -33,6 +33,7 @@ import json from .widget import Widget, _remove_buffers from .domwidget import DOMWidget +from .widget_link import Link snippet_template = """ @@ -95,31 +96,19 @@ def get_recursive_state(widget, store=None, drop_defaults=False): return store -def add_referring_widgets(store, drop_defaults): - """Add state of any widgets referring to widgets already in the store""" - found_new = False +def add_resolved_links(store, drop_defaults): + """Checks if any link models exists between models in store""" for widget_id, widget in Widget.widgets.items(): # go over all widgets - if widget_id not in store: - widget_state = widget.get_state(drop_defaults=drop_defaults) - widget_state = _remove_buffers(widget_state)[0] - # Loop over all references in current widget state: - for ref in _find_widget_refs_by_state(widget, widget_state): - # If the found ref is already in state, include the found reference - if ref.model_id in store: - store[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults) - found_new = True - return found_new - - -def dependency_state(widgets, drop_defaults, dependents=True): - """Get the state of all widgets specified, and their dependencies. + if isinstance(widget, Link) and widget_id not in store: + if widget.source[0].model_id in store and widget.target[0].model_id in store: + store[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults) + - If `dependents` is True (the default), widgets which depend on any of the - resolved widgets will be added as well. +def dependency_state(widgets, drop_defaults): + """Get the state of all widgets specified, and their dependencies. - In the below graph, D and E are depencies of C; A and B are dependents of C; - and F is an dependent of E. That means the state will include (C, D, E) for - dependents=False, and (A, B, C, D, E, F) for dependents=True. + In the below graph, D and E are depencies of C; C is a dependency of both A and B; + and E is a dependency of F. That means the state of C will include (C, D, E). A -- -- D | -- C -- | @@ -128,24 +117,26 @@ def dependency_state(widgets, drop_defaults, dependents=True): F -- ---- Dependecy ----> + + Note: Any links between included widgets will also be added. Using the example + above, this means that any links between widgets (C, D, E) will also be included. + """ # collect the state of all relevant widgets if widgets is None: + # Get state of all widgets, no smart resolution needed. widgets = Widget.widgets.values() state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=widgets)['state'] else: state = {} for widget in widgets: get_recursive_state(widget, state, drop_defaults) - if dependents: - # it may be that other widgets refer to the collected widgets, - # such as layouts, include those as well - while add_referring_widgets(state, drop_defaults): - pass + # Add any links between included widgets: + add_resolved_links(state, drop_defaults) return state -def embed_data(widgets, expand_dependencies='full', drop_defaults=True): +def embed_data(widgets, include_dependencies=True, drop_defaults=True): """Gets data for embedding. Use this to get the raw data for embedding if you have special @@ -160,9 +151,8 @@ def embed_data(widgets, expand_dependencies='full', drop_defaults=True): widgets[0] except (IndexError, TypeError): widgets = [widgets] - if expand_dependencies in ('full', 'partial'): - dependents = expand_dependencies == 'full' - state = dependency_state(widgets, drop_defaults, dependents=dependents) + if include_dependencies: + state = dependency_state(widgets, drop_defaults) else: state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=widgets)['state'] @@ -180,14 +170,14 @@ def embed_data(widgets, expand_dependencies='full', drop_defaults=True): def embed_snippet(widgets, - expand_dependencies='full', + include_dependencies=True, drop_defaults=True, indent=2, embed_url=None, ): """Return a snippet that can be embedded in an HTML file. """ - data = embed_data(widgets, expand_dependencies, drop_defaults) + data = embed_data(widgets, include_dependencies, drop_defaults) widget_views = '\n'.join( widget_view_template.format(**dict(view_spec=json.dumps(view_spec))) From d4848635090c79481c8bec4fa8344a33f4b84f53 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 29 May 2017 16:55:14 +0200 Subject: [PATCH 12/28] Add a more complex test --- ipywidgets/widgets/tests/test_embed.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ipywidgets/widgets/tests/test_embed.py b/ipywidgets/widgets/tests/test_embed.py index 4c96052973..bf3fb913cc 100644 --- a/ipywidgets/widgets/tests/test_embed.py +++ b/ipywidgets/widgets/tests/test_embed.py @@ -64,6 +64,41 @@ def test_embed_data_two_widgets(self): assert 'IntTextModel' in model_names assert 'IntSliderModel' in model_names + def test_embed_data_complex(self): + w1 = IntText(4) + w2 = IntSlider(min=0, max=100) + jslink((w1, 'value'), (w2, 'value')) + + w3 = CaseWidget() + w3.a = w1 + + w4 = CaseWidget() + w4.a = w3 + w4.other['test'] = w2 + + # Add a circular reference: + w3.b = w4 + + # Put it in an HBox + HBox(children=[w4]) + + data = embed_data(widgets=w4, drop_defaults=True) + + state = data['manager_state']['state'] + views = data['view_specs'] + + assert len(state) == 9 + assert len(views) == 1 + + model_names = [s['model_name'] for s in state.values()] + assert 'IntTextModel' in model_names + assert 'IntSliderModel' in model_names + assert 'CaseWidgetModel' in model_names + assert 'LinkModel' in model_names + + # Check that HBox is not collected + assert 'HBoxModel' not in model_names + def test_snippet(self): w = IntText(4) snippet = embed_snippet(widgets=w, drop_defaults=True) From a2c7ae414541e5826e2358a86534c46912862f5f Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Tue, 30 May 2017 11:00:49 +0200 Subject: [PATCH 13/28] Mark literals unicode for py2 compat --- ipywidgets/widgets/embed.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py index 5ca5e59a52..36b227678e 100644 --- a/ipywidgets/widgets/embed.py +++ b/ipywidgets/widgets/embed.py @@ -36,7 +36,7 @@ from .widget_link import Link -snippet_template = """ +snippet_template = u""" @@ -44,7 +44,7 @@ """ -html_template = """ +html_template = u""" @@ -56,7 +56,7 @@ """ -widget_view_template = """""" @@ -179,14 +179,14 @@ def embed_snippet(widgets, data = embed_data(widgets, include_dependencies, drop_defaults) - widget_views = '\n'.join( + widget_views = u'\n'.join( widget_view_template.format(**dict(view_spec=json.dumps(view_spec))) for view_spec in data['view_specs'] ) if embed_url is None: # TODO: Get widgets npm version automatically: - embed_url = 'https://unpkg.com/jupyter-js-widgets@~3.0.0-alpha.0/dist/embed.js' + embed_url = u'https://unpkg.com/jupyter-js-widgets@~3.0.0-alpha.0/dist/embed.js' values = { 'embed_url': embed_url, @@ -206,7 +206,7 @@ def embed_minimal_html(fp, widgets, **kwargs): snippet = embed_snippet(widgets, **kwargs) values = { - 'title': 'IPyWidget export', + 'title': u'IPyWidget export', 'snippet': snippet, } From c71c97886a6c235f756785a62b1ae4c636d94648 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Tue, 30 May 2017 11:48:15 +0200 Subject: [PATCH 14/28] Clean up embed interface and docstrings --- ipywidgets/widgets/embed.py | 87 +++++++++++++++++++------- ipywidgets/widgets/tests/test_embed.py | 12 ++-- 2 files changed, 70 insertions(+), 29 deletions(-) diff --git a/ipywidgets/widgets/embed.py b/ipywidgets/widgets/embed.py index 36b227678e..44eba2f567 100644 --- a/ipywidgets/widgets/embed.py +++ b/ipywidgets/widgets/embed.py @@ -31,7 +31,7 @@ """ import json -from .widget import Widget, _remove_buffers +from .widget import Widget from .domwidget import DOMWidget from .widget_link import Link @@ -107,8 +107,9 @@ def add_resolved_links(store, drop_defaults): def dependency_state(widgets, drop_defaults): """Get the state of all widgets specified, and their dependencies. - In the below graph, D and E are depencies of C; C is a dependency of both A and B; - and E is a dependency of F. That means the state of C will include (C, D, E). + In the below graph, D and E are depencies of C; C is a dependency of + both A and B; and E is a dependency of F. That means the state of C + will include (C, D, E). A -- -- D | -- C -- | @@ -118,8 +119,9 @@ def dependency_state(widgets, drop_defaults): ---- Dependecy ----> - Note: Any links between included widgets will also be added. Using the example - above, this means that any links between widgets (C, D, E) will also be included. + Note: Any links between included widgets will also be added. Using the + example above, this means that any links between widgets (C, D, E) will + also be included. """ # collect the state of all relevant widgets @@ -136,48 +138,87 @@ def dependency_state(widgets, drop_defaults): return state -def embed_data(widgets, include_dependencies=True, drop_defaults=True): +def embed_data(views, include_all=True, drop_defaults=True): """Gets data for embedding. Use this to get the raw data for embedding if you have special formatting needs. - Returns a dictionary with the following entries: + Parameters + ---------- + views: widget or collection of widgets or None + The widgets to include views for. If None, all DOMWidgets are + included (not just the displayed ones). + include_all: boolean + Which other widgets' state to include. When set to True, the state + of all widgets know to the widget manager is included. When False, + the dependencies of the given views are (attempted) resolved + automatically, and only their state is included. + drop_defaults: boolean + Whether to drop default values from the widget states. + + Returns + ------- + A dictionary with the following entries: manager_state: dict of the widget manager state data view_specs: a list of widget view specs """ - if widgets is not None: + if views is not None: try: - widgets[0] + views[0] except (IndexError, TypeError): - widgets = [widgets] - if include_dependencies: - state = dependency_state(widgets, drop_defaults) + views = [views] + if include_all: + state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=None)['state'] else: - state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=widgets)['state'] + state = dependency_state(views, drop_defaults) # Rely on ipywidget to get the default values json_data = Widget.get_manager_state(widgets=[]) # but plug in our own state json_data['state'] = state - if widgets is None: - widgets = [w for w in Widget.widgets.values() if isinstance(w, DOMWidget)] + if views is None: + views = [w for w in Widget.widgets.values() if isinstance(w, DOMWidget)] - view_specs = [w.get_view_spec() for w in widgets] + view_specs = [w.get_view_spec() for w in views] return dict(manager_state=json_data, view_specs=view_specs) -def embed_snippet(widgets, - include_dependencies=True, +def embed_snippet(views, + include_all=True, drop_defaults=True, indent=2, embed_url=None, ): - """Return a snippet that can be embedded in an HTML file. """ + """Return a snippet that can be embedded in an HTML file. + + Parameters + ---------- + views: widget or collection of widgets or None + The widgets to include views for. If None, all DOMWidgets are + included (not just the displayed ones). + include_all: boolean + Which other widgets' state to include. When set to True, the state + of all widgets know to the widget manager is included. When False, + the dependencies of the given views are (attempted) resolved + automatically, and only their state is included. + drop_defaults: boolean + Whether to drop default values from the widget states. + indent: integer, string or None + The indent to use for the JSON state dump. See `json.dumps` for + full description. + embed_url: string or None + Allows for overriding the URL used to fetch the widget manager + for the embedded code. This defaults (None) to an `unpkg` CDN url. + + Returns + ------- + A unicode string with an HTML snippet containing several ` diff --git a/ipywidgets/tests/__init__.py b/ipywidgets/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ipywidgets/widgets/tests/test_embed.py b/ipywidgets/tests/test_embed.py similarity index 100% rename from ipywidgets/widgets/tests/test_embed.py rename to ipywidgets/tests/test_embed.py From a1d903365481f87fd6df8b1a5b77327534abd992 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Thu, 22 Jun 2017 17:13:33 +0200 Subject: [PATCH 22/28] Cleanup embed docs recommonmark rst eval is not enabled, so fix the formatting --- docs/source/embedding.md | 50 +++++++++++++++++++--------------------- docs/source/index.rst | 2 +- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/docs/source/embedding.md b/docs/source/embedding.md index a301ec4343..76fa93fa50 100644 --- a/docs/source/embedding.md +++ b/docs/source/embedding.md @@ -55,25 +55,25 @@ corresponding to the same JSON schema. Embeddable code for the widgets can also be produced from the Python side. The following functions are available in the module `ipywidgets.embed`: -```rst -.. embed_snippet:: - +- `embed_snippet`: + ```py s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40) print(embed_snippet(views=[s1, s2])) + ``` -.. embed_data:: - +- `embed_data`: + ```py s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40) data = embed_data(views=[s1, s2]) print(data['manager_state']) print(data['view_specs']) + ``` -.. embed_minimal_html:: - +- `embed_minimal_html`: + ```py s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40) embed_minimal_html('my_export.html', views=[s1, s2]) - -``` + ``` Here, `embed_snippet` will return an embeddable HTML snippet similar to the Notebook interface detailed above, while `embed_data` will return the widget state JSON as @@ -85,17 +85,15 @@ In all functions, the state of all widgets known to the widget manager is included by default. You can alternatively pass a reduced state to use instead. This can be particularly relevant if you have many independent widgets with a large state, but only want to include the relevant ones in your export. To -include only the state of the views and their dependencies, use the function: - -```rst -.. dependency_state:: - - s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40) - print(embed_snippet( - views=[s1, s2], - state=dependency_state([s1, s2]), - )) - +include only the state of the views and their dependencies, use the function +`dependency_state`: + +```py +s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40) +print(embed_snippet( + views=[s1, s2], + state=dependency_state([s1, s2]), + )) ``` @@ -121,18 +119,18 @@ Two directives are provided: `ipywidgets-setup` and `ipywidgets-display`. `ipywidgets-setup` code is used to run potential boilerplate and configuration code prior to running the display code. For example: -```rst -.. ipywidgets-setup:: - +- `ipywidgets-setup`: + ```py from ipywidgets import VBox, jsdlink, IntSlider, Button + ``` -.. ipywidgets-display:: - +- `ipywidgets-display`: + ```py s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40) b = Button(icon='legal') jsdlink((s1, 'value'), (s2, 'max')) VBox([s1, s2, b]) -``` + ``` In the case of the `ipywidgets-display` code, the *last statement* of the code-block should contain the widget object you wish to be rendered. diff --git a/docs/source/index.rst b/docs/source/index.rst index 654394f75b..fa7093ca96 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -2,7 +2,7 @@ ipywidgets ========== Full Table of Contents --------- +---------------------- .. toctree:: :maxdepth: 2 From ea2c563ffd1f1b5aaaf317469b04e0697299dc1f Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Thu, 22 Jun 2017 17:28:04 +0200 Subject: [PATCH 23/28] Fix A change was left behind in the editor --- ipywidgets/tests/test_embed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipywidgets/tests/test_embed.py b/ipywidgets/tests/test_embed.py index b0db957d8e..7a0274f8cc 100644 --- a/ipywidgets/tests/test_embed.py +++ b/ipywidgets/tests/test_embed.py @@ -7,7 +7,7 @@ import traitlets -from .. import IntSlider, IntText, Widget, jslink, HBox, widget_serialization +from ..widgets import IntSlider, IntText, Widget, jslink, HBox, widget_serialization from ..embed import embed_data, embed_snippet, embed_minimal_html, dependency_state try: From 5c4d3eed5eb8ddd822be58c83bf32d84aaaea009 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Thu, 22 Jun 2017 17:53:02 +0200 Subject: [PATCH 24/28] Embed-py: Include import example in docs --- docs/source/embedding.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/embedding.md b/docs/source/embedding.md index 76fa93fa50..eefb8c7e0d 100644 --- a/docs/source/embedding.md +++ b/docs/source/embedding.md @@ -57,6 +57,8 @@ The following functions are available in the module `ipywidgets.embed`: - `embed_snippet`: ```py + from ipywidgets.embed import embed_snippet + s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40) print(embed_snippet(views=[s1, s2])) ``` From 0e14be44a776e1f881b93df542ed659a2623a01c Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Fri, 23 Jun 2017 12:52:19 +0200 Subject: [PATCH 25/28] Relicense code from ipyvolume https://github.com/jupyter-widgets/ipywidgets/pull/1387#issuecomment-310433484 "consider all code in this PR as a new contribution or consider the code to be relicensed under bsd." --- ipywidgets/embed.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/ipywidgets/embed.py b/ipywidgets/embed.py index 27b23694d8..d5923053bf 100644 --- a/ipywidgets/embed.py +++ b/ipywidgets/embed.py @@ -1,30 +1,5 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -# -# -# Parts of this code is copied from IPyVolume (24.05.2017), under the following license: -# -# MIT License -# -# Copyright (c) 2016 Maarten Breddels -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. """ Functions for generating embeddable HTML/javascript of a widget. From 09b4da88ecb3e88da18a72c7007212ad454728eb Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Fri, 23 Jun 2017 13:16:01 +0200 Subject: [PATCH 26/28] Clarify license --- ipywidgets/embed.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ipywidgets/embed.py b/ipywidgets/embed.py index d5923053bf..1cf3d8565c 100644 --- a/ipywidgets/embed.py +++ b/ipywidgets/embed.py @@ -1,5 +1,9 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +# +# +# Parts of this code is originally copied from IPyVolume (24.05.2017), +# relicensed under the BSD license. """ Functions for generating embeddable HTML/javascript of a widget. From 287eac049380eb44a589bfe18df15781333c342d Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 23 Jun 2017 10:58:50 -0400 Subject: [PATCH 27/28] Update permission statement. --- ipywidgets/embed.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ipywidgets/embed.py b/ipywidgets/embed.py index 1cf3d8565c..118dc60546 100644 --- a/ipywidgets/embed.py +++ b/ipywidgets/embed.py @@ -2,8 +2,9 @@ # Distributed under the terms of the Modified BSD License. # # -# Parts of this code is originally copied from IPyVolume (24.05.2017), -# relicensed under the BSD license. +# Parts of this code is from IPyVolume (24.05.2017), used here under +# this copyright and license with permission from the author +# (see https://github.com/jupyter-widgets/ipywidgets/pull/1387) """ Functions for generating embeddable HTML/javascript of a widget. From ce13271029daf83140ea0c703378269834d894d8 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Thu, 29 Jun 2017 15:39:10 +0200 Subject: [PATCH 28/28] Update the python embed URL to match JS one Also copy over the TODO --- ipywidgets/embed.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ipywidgets/embed.py b/ipywidgets/embed.py index 118dc60546..f12d9f84ae 100644 --- a/ipywidgets/embed.py +++ b/ipywidgets/embed.py @@ -39,6 +39,11 @@ {view_spec} """ +# TODO: This always points to the latest version of the html-manager. A better strategy +# would be to point to a version of the html-manager that has been tested with this +# release of jupyter-widgets/controls. +DEFAULT_EMBED_SCRIPT_URL = u'https://unpkg.com/@jupyter-widgets/html-manager@*/dist/index.js' + def _find_widget_refs_by_state(widget, state): """Find references to other widgets in a widget's state""" @@ -205,8 +210,7 @@ def embed_snippet(views, ) if embed_url is None: - # TODO: Get widgets npm version automatically: - embed_url = u'https://unpkg.com/jupyter-js-widgets@~3.0.0-alpha.0/dist/embed.js' + embed_url = DEFAULT_EMBED_SCRIPT_URL values = { 'embed_url': embed_url,