diff --git a/examples/user_guide/Deploy_and_Export.ipynb b/examples/user_guide/Deploy_and_Export.ipynb index cc0ee478c1..8efce3fa4e 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 rather than using Bokeh's internal communication mechanisms. You can enable ipywidgets support globally using:\n", + "\n", + "```python\n", + "pn.extension(comms='ipywidgets')\n", + "# or\n", + "pn.config.comms = 'ipywidgets'\n", + "```\n", + "\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", + "\n", + "```python\n", + "from ipywidgets import Accordion\n", + "Accordion(children=[pn.ipywidget(pane)])\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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", + "jupyter labextension install @bokeh/jupyter_bokeh\n", + "```" + ] + }, { "cell_type": "markdown", "metadata": {}, 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 7b16e46a14..00b7958381 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.""") + _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.""") + _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 comms(self): + if self._comms_ is not None: + return self._comms_ + else: + return os.environ.get('PANEL_COMMS', _config._comms) + + @comms.setter + def comms(self, value): + validate_config(self, '_comms', value) + self._comms_ = 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/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 ca9fdc6b40..6be139c759 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -277,6 +277,20 @@ 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.comms == 'ipywidgets': + ipywidget = self.ipywidget() + data = {} + 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. ' @@ -647,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