From 8aea9b673909644ee9cd9a8df2c4e396123ff2dc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 6 Nov 2019 11:36:41 -0600 Subject: [PATCH 1/5] Add support for jupyter_bokeh ipywidget rendering --- panel/config.py | 36 ++++++++++++++++++++++++++++++++++++ panel/viewable.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/panel/config.py b/panel/config.py index 7b16e46a14..0cf5f6d52e 100644 --- a/panel/config.py +++ b/panel/config.py @@ -27,6 +27,19 @@ _PATH = os.path.abspath(os.path.dirname(__file__)) _CSS_FILES = glob.glob(os.path.join(_PATH, '_styles', '*.css')) +def validate_config(config, parameter, value): + """ + Validates parameter setting on a hidden config parameter. + """ + orig = getattr(config, parameter) + try: + setattr(config, parameter, value) + except Exception as e: + raise e + finally: + setattr(config, parameter, orig) + + class _config(param.Parameterized): """ Holds global configuration options for Panel. The options can be @@ -66,6 +79,11 @@ class _config(param.Parameterized): _embed_load_path = param.String(default=None, doc=""" Where to load json files for embedded state.""") + _jupyter_ext = param.ObjectSelector( + default='default', objects=['default', 'ipywidgets'], doc=""" + Whether to render output in Jupyter with the default Jupyter + extension or use the jupyter_bokeh ipywidget model.""") + _inline = param.Boolean(default=True, allow_None=True, doc=""" Whether to inline JS and CSS resources. If disabled, resources are loaded from CDN if one is available.""") @@ -100,8 +118,21 @@ def embed(self): @embed.setter def embed(self, value): + validate_config(self, '_embed', value) self._embed_ = value + @property + def jupyter_ext(self): + if self._jupyter_ext_ is not None: + return self._jupyter_ext_ + else: + return os.environ.get('PANEL_JUPYTER_EXT', _config._jupyter_ext) + + @jupyter_ext.setter + def jupyter_ext(self, value): + validate_config(self, '_jupyter_ext', value) + self._jupyter_ext_ = value + @property def embed_json(self): if self._embed_json_ is not None: @@ -111,6 +142,7 @@ def embed_json(self): @embed_json.setter def embed_json(self, value): + validate_config(self, '_embed_json', value) self._embed_json_ = value @property @@ -122,6 +154,7 @@ def embed_json_prefix(self): @embed_json_prefix.setter def embed_json_prefix(self, value): + validate_config(self, '_embed_json_prefix', value) self._embed_json_prefix_ = value @property @@ -133,6 +166,7 @@ def embed_save_path(self): @embed_save_path.setter def embed_save_path(self, value): + validate_config(self, '_embed_save_path', value) self._embed_save_path_ = value @property @@ -144,6 +178,7 @@ def embed_load_path(self): @embed_load_path.setter def embed_load_path(self, value): + validate_config(self, '_embed_load_path', value) self._embed_load_path_ = value @property @@ -155,6 +190,7 @@ def inline(self): @inline.setter def inline(self, value): + validate_config(self, '_inline', value) self._inline_ = value diff --git a/panel/viewable.py b/panel/viewable.py index ca9fdc6b40..a1ff7d9317 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -277,6 +277,25 @@ def _repr_mimebundle_(self, include=None, exclude=None): if not loaded and 'holoviews' in sys.modules: import holoviews as hv loaded = hv.extension._loaded + + if config.jupyter_ext == 'ipywidgets': + ipywidget = self.ipywidget() + plaintext = repr(ipywidget) + if len(plaintext) > 110: + plaintext = plaintext[:110] + '…' + data = { + 'text/plain': plaintext, + } + if ipywidget._view_name is not None: + data['application/vnd.jupyter.widget-view+json'] = { + 'version_major': 2, + 'version_minor': 0, + 'model_id': ipywidget._model_id + } + if ipywidget._view_name is not None: + ipywidget._handle_displayed() + return data, {} + if not loaded: self.param.warning('Displaying Panel objects in the notebook ' 'requires the panel extension to be loaded. ' @@ -421,6 +440,18 @@ def get_root(self, doc=None, comm=None): state._views[ref] = (self, root, doc, comm) return root + def ipywidget(self): + """ + Creates a root model from the Panel object and wraps it in + a jupyter_bokeh ipywidget BokehModel. + + Returns + ------- + Returns an ipywidget model which renders the Panel object. + """ + from jupyter_bokeh import BokehModel + return BokehModel(self.get_root()) + def save(self, filename, title=None, resources=None, template=None, template_variables=None, embed=False, max_states=1000, max_opts=3, embed_json=False, json_prefix='', save_path='./', From 4a93941bdc31015f727dcc4f14df15d052a8816f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 6 Nov 2019 13:37:57 -0600 Subject: [PATCH 2/5] Applied review comments --- panel/__init__.py | 2 +- panel/config.py | 18 +++++++++--------- panel/io/__init__.py | 2 +- panel/io/notebook.py | 13 +++++++++++++ panel/viewable.py | 14 +------------- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/panel/__init__.py b/panel/__init__.py index a754ceca42..756bc0e1b2 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -11,7 +11,7 @@ from .config import config, panel_extension as extension # noqa from .interact import interact # noqa -from .io import state # noqa +from .io import ipywidget, state # noqa from .layout import Row, Column, WidgetBox, Tabs, Spacer, GridSpec, GridBox # noqa from .pane import panel, Pane # noqa from .param import Param # noqa diff --git a/panel/config.py b/panel/config.py index 0cf5f6d52e..00b7958381 100644 --- a/panel/config.py +++ b/panel/config.py @@ -79,7 +79,7 @@ class _config(param.Parameterized): _embed_load_path = param.String(default=None, doc=""" Where to load json files for embedded state.""") - _jupyter_ext = param.ObjectSelector( + _comms = param.ObjectSelector( default='default', objects=['default', 'ipywidgets'], doc=""" Whether to render output in Jupyter with the default Jupyter extension or use the jupyter_bokeh ipywidget model.""") @@ -122,16 +122,16 @@ def embed(self, value): self._embed_ = value @property - def jupyter_ext(self): - if self._jupyter_ext_ is not None: - return self._jupyter_ext_ + def comms(self): + if self._comms_ is not None: + return self._comms_ else: - return os.environ.get('PANEL_JUPYTER_EXT', _config._jupyter_ext) + return os.environ.get('PANEL_COMMS', _config._comms) - @jupyter_ext.setter - def jupyter_ext(self, value): - validate_config(self, '_jupyter_ext', value) - self._jupyter_ext_ = value + @comms.setter + def comms(self, value): + validate_config(self, '_comms', value) + self._comms_ = value @property def embed_json(self): diff --git a/panel/io/__init__.py b/panel/io/__init__.py index 3918b7b67e..8f8f859002 100644 --- a/panel/io/__init__.py +++ b/panel/io/__init__.py @@ -8,4 +8,4 @@ from .model import add_to_doc, remove_root, diff # noqa from .resources import Resources # noqa from .server import get_server # noqa -from .notebook import block_comm, load_notebook, push # noqa +from .notebook import block_comm, ipywidget, load_notebook, push # noqa diff --git a/panel/io/notebook.py b/panel/io/notebook.py index e77ac31413..0b9cd090e8 100644 --- a/panel/io/notebook.py +++ b/panel/io/notebook.py @@ -375,3 +375,16 @@ def show_embed(panel, max_states=1000, max_opts=3, json=False, embed_state(panel, model, doc, max_states, max_opts, json, save_path, load_path) publish_display_data(*render_model(model)) + + +def ipywidget(panel): + """ + Creates a root model from the Panel object and wraps it in + a jupyter_bokeh ipywidget BokehModel. + + Returns + ------- + Returns an ipywidget model which renders the Panel object. + """ + from jupyter_bokeh import BokehModel + return BokehModel(panel.get_root()) diff --git a/panel/viewable.py b/panel/viewable.py index a1ff7d9317..9e3b3e6507 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -278,7 +278,7 @@ def _repr_mimebundle_(self, include=None, exclude=None): import holoviews as hv loaded = hv.extension._loaded - if config.jupyter_ext == 'ipywidgets': + if config.comms == 'ipywidgets': ipywidget = self.ipywidget() plaintext = repr(ipywidget) if len(plaintext) > 110: @@ -440,18 +440,6 @@ def get_root(self, doc=None, comm=None): state._views[ref] = (self, root, doc, comm) return root - def ipywidget(self): - """ - Creates a root model from the Panel object and wraps it in - a jupyter_bokeh ipywidget BokehModel. - - Returns - ------- - Returns an ipywidget model which renders the Panel object. - """ - from jupyter_bokeh import BokehModel - return BokehModel(self.get_root()) - def save(self, filename, title=None, resources=None, template=None, template_variables=None, embed=False, max_states=1000, max_opts=3, embed_json=False, json_prefix='', save_path='./', From 7d5e1374cf539b56875cd7e542ebc941b7e2521f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 6 Nov 2019 13:48:25 -0600 Subject: [PATCH 3/5] Add docs --- examples/user_guide/Deploy_and_Export.ipynb | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/examples/user_guide/Deploy_and_Export.ipynb b/examples/user_guide/Deploy_and_Export.ipynb index cc0ee478c1..9e070a7e73 100644 --- a/examples/user_guide/Deploy_and_Export.ipynb +++ b/examples/user_guide/Deploy_and_Export.ipynb @@ -129,6 +129,47 @@ "The app will now run on a Bokeh server instance separate from the Jupyter notebook kernel, allowing you to quickly test that all the functionality of your app works both in a notebook and in a server context." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### ipywidgets\n", + "\n", + "If the `jupyter_bokeh` package is installed it is also possible to render Panel objects as an ipywidget, either by enabling it globally using:\n", + "\n", + "```python\n", + "pn.extension(comms='ipywidgets')\n", + "# or\n", + "pn.config.comms = 'ipywidgets'\n", + "```\n", + "\n", + "This approach can be useful when trying to serve an entire notebook using [Voilà](https://github.com/voila-dashboards/voila). Alternatively we can convert an each individual object to an ipywidget using the `pn.ipywidget` function:\n", + "\n", + "```python\n", + "ipywidget = pn.ipywidget(pane)\n", + "ipywidget\n", + "```\n", + "\n", + "This approach also allows combining a Panel object with any other Jupyter widget based model:\n", + "\n", + "```python\n", + "from ipywidgets import Accordion\n", + "Accordion(children=[pn.ipywidget(pane)])\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In JupyterLab the following extensions have to be installed:\n", + " \n", + "```\n", + "jupyter labextension install @jupyter-widgets/jupyterlab-manager\n", + "jupyter labextension install @bokeh/jupyter_bokeh\n", + "```" + ] + }, { "cell_type": "markdown", "metadata": {}, From aeededf77266dc6e1e72375a240183f2b73d5752 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 6 Nov 2019 14:29:50 -0600 Subject: [PATCH 4/5] Apply suggestions from code review Co-Authored-By: James A. Bednar --- examples/user_guide/Deploy_and_Export.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/user_guide/Deploy_and_Export.ipynb b/examples/user_guide/Deploy_and_Export.ipynb index 9e070a7e73..8efce3fa4e 100644 --- a/examples/user_guide/Deploy_and_Export.ipynb +++ b/examples/user_guide/Deploy_and_Export.ipynb @@ -135,7 +135,7 @@ "source": [ "### ipywidgets\n", "\n", - "If the `jupyter_bokeh` package is installed it is also possible to render Panel objects as an ipywidget, either by enabling it globally using:\n", + "If the `jupyter_bokeh` package is installed it is also possible to render Panel objects as an ipywidget rather than using Bokeh's internal communication mechanisms. You can enable ipywidgets support globally using:\n", "\n", "```python\n", "pn.extension(comms='ipywidgets')\n", @@ -143,14 +143,14 @@ "pn.config.comms = 'ipywidgets'\n", "```\n", "\n", - "This approach can be useful when trying to serve an entire notebook using [Voilà](https://github.com/voila-dashboards/voila). Alternatively we can convert an each individual object to an ipywidget using the `pn.ipywidget` function:\n", + "This global setting can be useful when trying to serve an entire notebook using [Voilà](https://github.com/voila-dashboards/voila). Alternatively, we can convert individual objects to an ipywidget one at a time using the `pn.ipywidget()` function:\n", "\n", "```python\n", "ipywidget = pn.ipywidget(pane)\n", "ipywidget\n", "```\n", "\n", - "This approach also allows combining a Panel object with any other Jupyter widget based model:\n", + "This approach also allows combining a Panel object with any other Jupyter-widget--based model:\n", "\n", "```python\n", "from ipywidgets import Accordion\n", @@ -162,7 +162,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In JupyterLab the following extensions have to be installed:\n", + "To use Panel's ipywidgets support in JupyterLab, the following extensions have to be installed:\n", " \n", "```\n", "jupyter labextension install @jupyter-widgets/jupyterlab-manager\n", From e4c8bb794c1f506ab5e86dbaf69566c14fc3c8ed Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 6 Nov 2019 15:17:00 -0600 Subject: [PATCH 5/5] Fixed py2 issue --- panel/viewable.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/panel/viewable.py b/panel/viewable.py index 9e3b3e6507..6be139c759 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -280,12 +280,7 @@ def _repr_mimebundle_(self, include=None, exclude=None): if config.comms == 'ipywidgets': ipywidget = self.ipywidget() - plaintext = repr(ipywidget) - if len(plaintext) > 110: - plaintext = plaintext[:110] + '…' - data = { - 'text/plain': plaintext, - } + data = {} if ipywidget._view_name is not None: data['application/vnd.jupyter.widget-view+json'] = { 'version_major': 2, @@ -295,7 +290,7 @@ def _repr_mimebundle_(self, include=None, exclude=None): if ipywidget._view_name is not None: ipywidget._handle_displayed() return data, {} - + if not loaded: self.param.warning('Displaying Panel objects in the notebook ' 'requires the panel extension to be loaded. ' @@ -666,7 +661,7 @@ def _link_props(self, model, properties, doc, root, comm=None): if comm is None: for p in properties: if isinstance(p, tuple): - p, _ = p + _, p = p model.on_change(p, partial(self._server_change, doc)) elif config.embed: pass