Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add String pane to support any representable object #21

Merged
merged 6 commits into from Sep 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 70 additions & 32 deletions panel/pane.py
Expand Up @@ -10,12 +10,18 @@
import base64
from io import BytesIO

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


import param

from bokeh.layouts import Row as _BkRow, WidgetBox as _BkWidgetBox
from bokeh.models import LayoutDOM, CustomJS, Widget as _BkWidget, Div as _BkDiv

from .util import basestring, get_method_owner, push, remove_root, Div
from .util import basestring, unicode, get_method_owner, push, remove_root, Div
from .viewable import Reactive, Viewable


Expand All @@ -32,7 +38,7 @@ class PaneBase(Reactive):
"""
PaneBase is the abstract baseclass for all atomic displayable units
in the panel library. Pane defines an extensible interface for
wrapping arbitrary objects and transforming them into bokeh models.
wrapping arbitrary objects and transforming them into Bokeh models.

Panes are reactive in the sense that when the object they are
wrapping is changed any dashboard containing the pane will update
Expand All @@ -43,12 +49,13 @@ class PaneBase(Reactive):
"""

object = param.Parameter(default=None, doc="""
The object being wrapped, which will be converted into a bokeh model.""")
The object being wrapped, which will be converted into a Bokeh model.""")

# When multiple Panes apply to an object the precedence is used
precedence = 0
# When multiple Panes apply to an object, the one with the highest
# numerical precedence is selected. The default is an intermediate value.
precedence = 0.5

# Declares whether Pane supports updates to the bokeh model
# Declares whether Pane supports updates to the Bokeh model
_updates = False

__abstract = True
Expand All @@ -66,7 +73,7 @@ def get_pane_type(cls, obj):
if isinstance(obj, Viewable):
return type(obj)
descendents = [(p.precedence, p) for p in param.concrete_descendents(PaneBase).values()]
pane_types = sorted(descendents, key=lambda x: x[0])
pane_types = reversed(sorted(descendents, key=lambda x: x[0]))
for _, pane_type in pane_types:
if not pane_type.applies(obj): continue
return pane_type
Expand Down Expand Up @@ -98,15 +105,15 @@ def _cleanup(self, model, final=False):

def _update(self, model):
"""
If _updates=True this method is used to update an existing bokeh
If _updates=True this method is used to update an existing Bokeh
model instead of replacing the model entirely. The supplied model
should be updated with the current state.
"""
raise NotImplementedError

def _link_object(self, model, doc, root, parent, comm=None):
"""
Links the object parameter to the rendered bokeh model, triggering
Links the object parameter to the rendered Bokeh model, triggering
an update when the object changes.
"""
def update_pane(change, history=[model]):
Expand Down Expand Up @@ -142,16 +149,18 @@ def update_models():

class Bokeh(PaneBase):
"""
Bokeh panes allow including any bokeh model in a panel.
Bokeh panes allow including any Bokeh model in a panel.
"""

precedence = 0.8

@classmethod
def applies(cls, obj):
return isinstance(obj, LayoutDOM)

def _get_model(self, doc, root=None, parent=None, comm=None):
"""
Should return the bokeh model to be rendered.
Should return the Bokeh model to be rendered.
"""
model = self.object
if isinstance(model, _BkWidget):
Expand All @@ -175,9 +184,11 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
class HoloViews(PaneBase):
"""
HoloViews panes render any HoloViews object to a corresponding
bokeh model while respecting the currently selected backend.
Bokeh model while respecting the currently selected backend.
"""


precedence = 0.8

@classmethod
def applies(cls, obj):
if 'holoviews' not in sys.modules:
Expand Down Expand Up @@ -213,7 +224,7 @@ def _cleanup(self, model, final=False):

def _get_model(self, doc, root=None, parent=None, comm=None):
"""
Should return the bokeh model to be rendered.
Should return the Bokeh model to be rendered.
"""
from holoviews import Store
renderer = Store.renderers[Store.current_backend]
Expand Down Expand Up @@ -408,24 +419,6 @@ def _png(self):
return b.getvalue()


class HTML(DivPaneBase):
"""
HTML panes render any object which has a _repr_html_ method and wraps
the HTML in a bokeh Div model. The height and width can optionally
be specified, to allow room for whatever is being wrapped.
"""

precedence = 1

@classmethod
def applies(cls, obj):
return hasattr(obj, '_repr_html_')

def _get_properties(self):
properties = super(HTML, self)._get_properties()
return dict(properties, text=self.object._repr_html_())


class RGGPlot(PNG):
"""
An RGGPlot panes render an r2py-based ggplot2 figure to png
Expand All @@ -450,3 +443,48 @@ def _png(self):
res=self.dpi, antialias="subpixel") as b:
robjects.r("print")(self.object)
return b.getvalue()


class HTML(DivPaneBase):
"""
HTML panes wrap HTML text in a bokeh Div model. The
provided object can either be a text string, or an object that
has a `_repr_html_` method that can be called to get the HTML
text string. The height and width can optionally be specified, to
allow room for whatever is being wrapped.
"""

precedence = 0.2

@classmethod
def applies(cls, obj):
return (hasattr(obj, '_repr_html_') or
(isinstance(obj, basestring) or
(isinstance(obj, unicode))))

def _get_properties(self):
properties = super(HTML, self)._get_properties()
text=self.object
if hasattr(text, '_repr_html_'):
text=text._repr_html_()
return dict(properties, text=text)


class Str(DivPaneBase):
"""
A Str pane renders any object for which `str()` can be called,
escaping any HTML markup and then wrapping the resulting string in
a bokeh Div model. Set to a low precedence because generally one
will want a better representation, but allows arbitrary objects to
be used as a Pane (numbers, arrays, objects, etc.).
"""

precedence = 0

@classmethod
def applies(cls, obj):
return True

def _get_properties(self):
properties = super(Str, self)._get_properties()
return dict(properties, text='<pre>'+escape(str(self.object))+'</pre>')
2 changes: 1 addition & 1 deletion panel/param.py
Expand Up @@ -57,7 +57,7 @@ class Param(PaneBase):
label_formatter = param.Callable(default=default_label_formatter, allow_None=True,
doc="Callable used to format the parameter names into widget labels.")

precedence = 1
precedence = 0.1

_mapping = {
param.Action: Button,
Expand Down
2 changes: 2 additions & 0 deletions panel/plotly.py
Expand Up @@ -36,6 +36,8 @@ class Plotly(PaneBase):

_updates = True

precedence = 0.8

def __init__(self, object, layout=None, **params):
super(Plotly, self).__init__(self._to_figure(object, layout),
layout=layout, **params)
Expand Down
54 changes: 53 additions & 1 deletion panel/tests/test_panes.py
Expand Up @@ -7,7 +7,7 @@
GlyphRenderer, Circle, Line)
from bokeh.plotting import Figure
from panel.pane import (Pane, PaneBase, Bokeh, HoloViews, Matplotlib,
ParamMethod)
ParamMethod, HTML, Str)

try:
import holoviews as hv
Expand Down Expand Up @@ -276,3 +276,55 @@ def test_param_method_pane_changing_type(document, comm):
# Cleanup pane
new_pane._cleanup(model)
assert new_pane._callbacks == {}


def test_get_html_pane_type():
assert PaneBase.get_pane_type("<h1>Test</h1>") is HTML


def test_html_pane(document, comm):
pane = Pane("<h1>Test</h1>")

# Create pane
row = pane._get_root(document, comm=comm)
assert isinstance(row, BkRow)
assert len(row.children) == 1
model = row.children[0]
assert model.ref['id'] in pane._callbacks
div = get_div(model)
assert div.text == "<h1>Test</h1>"

# Replace Pane.object
pane.object = "<h2>Test</h2>"
model = row.children[0]
assert div is get_div(model)
assert model.ref['id'] in pane._callbacks
assert div.text == "<h2>Test</h2>"

# Cleanup
pane._cleanup(model)
assert pane._callbacks == {}


def test_string_pane(document, comm):
pane = Str("<h1>Test</h1>")

# Create pane
row = pane._get_root(document, comm=comm)
assert isinstance(row, BkRow)
assert len(row.children) == 1
model = row.children[0]
assert model.ref['id'] in pane._callbacks
div = get_div(model)
assert div.text == "<pre>&lt;h1&gt;Test&lt;/h1&gt;</pre>"

# Replace Pane.object
pane.object = "<h2>Test</h2>"
model = row.children[0]
assert div is get_div(model)
assert model.ref['id'] in pane._callbacks
assert div.text == "<pre>&lt;h2&gt;Test&lt;/h2&gt;</pre>"

# Cleanup
pane._cleanup(model)
assert pane._callbacks == {}