Skip to content

Commit

Permalink
Add support for displaying callback errors and stdout in the notebook (
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Jan 22, 2020
1 parent cfdecb0 commit 33ae2e5
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 29 deletions.
50 changes: 47 additions & 3 deletions examples/user_guide/Overview.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"\n",
"##### ``pn.extension()``\n",
"\n",
"> The Panel extension loads BokehJS, any custom models required, and optionally additional custom JS and CSS in Jupyter notebook environments.\n",
"> The Panel extension loads BokehJS, any custom models required, and optionally additional custom JS and CSS in Jupyter notebook environments. It also allows passing any [`pn.config`](#pn.config) variables \n",
"\n",
"##### ``pn.ipywidget()``\n",
"\n",
Expand All @@ -84,7 +84,7 @@
"\n",
"##### ``pn.serve()``\n",
"\n",
"Similar to .show() on a Panel object but allows serving one or more Panel apps on a single server. Supplying a dictionary mapping from the URL slugs to the individual Panel objects being served allows launching multiple apps at once. \n",
">Similar to .show() on a Panel object but allows serving one or more Panel apps on a single server. Supplying a dictionary mapping from the URL slugs to the individual Panel objects being served allows launching multiple apps at once. \n",
"\n",
"#### Command line\n",
"\n",
Expand Down Expand Up @@ -139,7 +139,51 @@
"\n",
"##### ``.jslink()``\n",
"\n",
"> The JavaScript-based ``.jslink()`` method directly links properties of the underlying Bokeh models, making it possible to define interactivity that works even without a running Python server."
"> The JavaScript-based ``.jslink()`` method directly links properties of the underlying Bokeh models, making it possible to define interactivity that works even without a running Python server.\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### State and configuration\n",
"\n",
"Panel provides top-level objects to hold current state and control high-level configuration variables.\n",
"\n",
"##### `pn.config`\n",
"\n",
">The `pn.config` object allows setting various configuration variables, the config variables can also be set as environment variables or passed through the [`pn.extension`](#pn.extension()):\n",
"\n",
"> #### Python only\n",
"> - `css_files` (: External CSS files to load.\n",
"> - `js_files`: External JS files to load. Dictionary should map from exported name to the URL of the JS file.\n",
"> - `raw_css`: List of raw CSS strings to add to load.\n",
"> - `sizing_mode`: Specify the default sizing mode behavior of panels.\n",
"\n",
"> #### Python and Environment variables\n",
"> - `comms` (`PANEL_COMMS`): Whether to render output in Jupyter with the default Jupyter extension or use the `jupyter_bokeh` ipywidget model.\n",
"> - `debug` (`PANEL_DEBUG`): How to log errors and stdout output triggered by callbacks from Javascript in the notebook. Options include `'accumulate'`, `'replace'` and `'disable'`.\n",
"> - `embed` (`PANEL_EMBED`): Whether plot data will be [embedded](./Deploy_and_Export.ipynb#Embedding).\n",
"> - `embed_json` (`PANEL_EMBED_JSON`): Whether to save embedded state to json files.\n",
"> - `embed_json_prefix` (`PANEL_EMBED_JSON_PREFIX`): Prefix for randomly generated json directories.\n",
"> - `embed_load_path` (`PANEL_EMBED_LOAD_PATH`): Where to load json files for embedded state.\n",
"> - `embed_save_path` (`PANEL_EMBED_SAVE_PATH`): Where to save json files for embedded state.\n",
"> - `inline` (`PANEL_INLINE`): Whether to inline JS and CSS resources. If disabled, resources are loaded from CDN if one is available.\n",
"\n",
"##### `pn.state`\n",
"\n",
"The `pn.state` object makes various global state available and provides methods to manage that state:\n",
"\n",
"> - `cache`: A global cache which can be used to share data between different processes.\n",
"> - `curdoc`: When running a server session this property holds the current bokeh Document. \n",
"> - `webdriver`: Caches the current webdriver to speed up export of bokeh models to PNGs.\n",
"> - `session_args`: When running a server session this return the request arguments.\n",
"\n",
"> #### Methods\n",
"\n",
"> - `kill_all_servers`: Stops all running server sessions."
]
}
],
Expand Down
45 changes: 32 additions & 13 deletions panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,30 @@ class _config(param.Parameterized):
"""

css_files = param.List(default=_CSS_FILES, doc="""
External CSS files to load as part of the template.""")
External CSS files to load.""")

js_files = param.Dict(default={}, doc="""
External JS files to load as part of the template. Dictionary
should map from exported name to the URL of the JS file.""")
External JS files to load. Dictionary should map from exported
name to the URL of the JS file.""")

raw_css = param.List(default=[], doc="""
List of raw CSS strings to add to the template.""")
List of raw CSS strings to add to load.""")

sizing_mode = param.ObjectSelector(default=None, objects=[
'fixed', 'stretch_width', 'stretch_height', 'stretch_both',
'scale_width', 'scale_height', 'scale_both', None], doc="""
Specify the default sizing mode behavior of panels.""")

_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.""")

_debug = param.ObjectSelector(default='accumulate', allow_None=True,
objects=['accumulate', 'replace', 'disable'], doc="""
How to log errors and stdout output triggered by callbacks
from Javascript in the notebook.""")

_embed = param.Boolean(default=False, allow_None=True, doc="""
Whether plot data will be embedded.""")

Expand All @@ -78,20 +88,15 @@ class _config(param.Parameterized):
_embed_json_prefix = param.String(default='', doc="""
Prefix for randomly generated json directories.""")

_embed_save_path = param.String(default='./', doc="""
Where to save json files for embedded state.""")

_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.""")
_embed_save_path = param.String(default='./', doc="""
Where to save json files for embedded state.""")

_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.""")
Whether to inline JS and CSS resources. If disabled, resources
are loaded from CDN if one is available.""")

_truthy = ['True', 'true', '1', True, 1]

Expand All @@ -114,6 +119,20 @@ def set(self, **kwargs):
for k, v in overrides:
setattr(self, k+'_', v)

@property
def debug(self):
if self._debug_ is not None:
return self._debug_
elif os.environ.get('PANEL_DOC_BUILD'):
return 'disable'
else:
return os.environ.get('PANEL_DEBUG', _config._debug)

@debug.setter
def debug(self, value):
validate_config(self, '_debug', value)
self._debug_ = value

@property
def embed(self):
if self._embed_ is not None:
Expand Down
7 changes: 6 additions & 1 deletion panel/io/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,18 @@ class _state(param.Parameterized):
# An index of all currently active servers
_servers = {}

# Jupyter display handles
_handles = {}

def __repr__(self):
server_info = []
for server, panel, docs in self._servers.values():
server_info.append("{}:{:d} - {!r}".format(
server.address or "localhost", server.port, panel)
)
return "state(servers=\n {}\n)".format(",\n ".join(server_info))
if not server_info:
return "state(servers=[])"
return "state(servers=[\n {}\n])".format(",\n ".join(server_info))

def kill_all_servers(self):
"""Stop all servers and clear them from the current state."""
Expand Down
7 changes: 2 additions & 5 deletions panel/pane/markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@
import json
import textwrap

try:
from html import escape
except:
from cgi import escape
from six import string_types

import param

from ..viewable import Layoutable
from ..models import HTML as _BkHTML, JSON as _BkJSON
from ..util import escape
from ..viewable import Layoutable
from .base import PaneBase


Expand Down
30 changes: 30 additions & 0 deletions panel/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
"""
from __future__ import absolute_import, division, unicode_literals

import os
import re
import shutil

import pytest

from contextlib import contextmanager

from bokeh.document import Document
from bokeh.client import pull_session
from pyviz_comms import Comm

from panel.pane import HTML, Markdown
from panel.io import state


@pytest.fixture
Expand Down Expand Up @@ -45,6 +49,20 @@ def hv_bokeh():
hv.Store.current_backend = prev_backend


@pytest.yield_fixture
def get_display_handle():
cleanup = []
def display_handle(model):
cleanup.append(model.ref['id'])
handle = {}
state._handles[model.ref['id']] = (handle, [])
return handle
yield display_handle
for ref in cleanup:
if ref in state._handles:
del state._handles[ref]


@pytest.yield_fixture
def hv_mpl():
import holoviews as hv
Expand Down Expand Up @@ -97,3 +115,15 @@ def markdown_server_session():
server.stop()
except AssertionError:
pass # tests may already close this


@contextmanager
def set_env_var(env_var, value):
old_value = os.environ.get(env_var)
os.environ[env_var] = value
yield
if old_value is None:
del os.environ[env_var]
else:
os.environ[env_var] = old_value

127 changes: 127 additions & 0 deletions panel/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Tests pn.config variables
"""
from __future__ import absolute_import, division, unicode_literals

from panel import config, state
from panel.pane import HTML

from panel.tests.conftest import set_env_var


def test_env_var_debug():
with set_env_var('PANEL_DEBUG', 'disable'):
assert config.debug == 'disable'
with set_env_var('PANEL_DEBUG', 'replace'):
assert config.debug == 'replace'
with set_env_var('PANEL_DOC_BUILD', 'true'):
assert config.debug == 'disable'


def test_debug_replace_stdout(document, comm, get_display_handle):
pane = HTML()
with set_env_var('PANEL_DEBUG', 'replace'):
model = pane.get_root(document, comm)
handle = get_display_handle(model)

pane._on_stdout(model.ref['id'], ['print output'])
assert handle == {'text/html': 'print output</br>', 'raw': True}

pane._on_stdout(model.ref['id'], ['new output'])
assert handle == {'text/html': 'new output</br>', 'raw': True}

pane._cleanup(model)
assert model.ref['id'] not in state._handles


def test_debug_accumulate_stdout(document, comm, get_display_handle):
pane = HTML()
model = pane.get_root(document, comm)
handle = get_display_handle(model)

pane._on_stdout(model.ref['id'], ['print output'])
assert handle == {'text/html': 'print output</br>', 'raw': True}

pane._on_stdout(model.ref['id'], ['new output'])
assert handle == {'text/html': 'print output</br>\nnew output</br>', 'raw': True}

pane._cleanup(model)
assert model.ref['id'] not in state._handles


def test_debug_disable_stdout(document, comm, get_display_handle):
pane = HTML()
with set_env_var('PANEL_DEBUG', 'disable'):
model = pane.get_root(document, comm)
handle = get_display_handle(model)

pane._on_stdout(model.ref['id'], ['print output'])
assert handle == {}

pane._cleanup(model)
assert model.ref['id'] not in state._handles


def test_debug_replace_error(document, comm, get_display_handle):
pane = HTML()
with set_env_var('PANEL_DEBUG', 'replace'):
model = pane.get_root(document, comm)
handle = get_display_handle(model)

try:
1/0
except Exception as e:
pane._on_error(model.ref['id'], e)
assert 'text/html' in handle
assert 'ZeroDivisionError' in handle['text/html']

try:
1 + '2'
except Exception as e:
pane._on_error(model.ref['id'], e)
assert 'text/html' in handle
assert 'ZeroDivisionError' not in handle['text/html']
assert 'TypeError' in handle['text/html']

pane._cleanup(model)
assert model.ref['id'] not in state._handles


def test_debug_accumulate_error(document, comm, get_display_handle):
pane = HTML()
model = pane.get_root(document, comm)
handle = get_display_handle(model)

try:
1/0
except Exception as e:
pane._on_error(model.ref['id'], e)
assert 'text/html' in handle
assert 'ZeroDivisionError' in handle['text/html']

try:
1 + '2'
except Exception as e:
pane._on_error(model.ref['id'], e)
assert 'text/html' in handle
assert 'ZeroDivisionError' in handle['text/html']
assert 'TypeError' in handle['text/html']

pane._cleanup(model)
assert model.ref['id'] not in state._handles


def test_debug_disable_error(document, comm, get_display_handle):
pane = HTML()
with set_env_var('PANEL_DEBUG', 'disable'):
model = pane.get_root(document, comm)
handle = get_display_handle(model)

try:
1/0
except Exception as e:
pane._on_error(model.ref['id'], e)
assert handle == {}

pane._cleanup(model)
assert model.ref['id'] not in state._handles
9 changes: 8 additions & 1 deletion panel/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@
import numbers
import datetime as dt


from collections import defaultdict, OrderedDict
from datetime import datetime
from six import string_types
from collections import defaultdict, OrderedDict

try: # python >= 3.3
from collections.abc import MutableSequence, MutableMapping
except ImportError:
from collections import MutableSequence, MutableMapping

try:
from html import escape # noqa
except:
from cgi import escape # noqa

import param
import numpy as np

Expand Down
Loading

0 comments on commit 33ae2e5

Please sign in to comment.