Skip to content

Commit

Permalink
Embed fixes and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Jan 27, 2020
1 parent 06ccad6 commit ac7d924
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 23 deletions.
40 changes: 24 additions & 16 deletions panel/io/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,28 @@ def param_to_jslink(model, widget):
Converts Param pane widget links into JS links if possible.
"""
from ..viewable import Reactive
from ..widgets import LiteralInput
from ..widgets import Widget, LiteralInput

param_pane = widget._param_pane
pobj = param_pane.object
pname = [k for k, v in param_pane._widgets.items() if v is widget]
watchers = [w for w in get_watchers(widget) if w not in widget._callbacks
and w not in param_pane._callbacks]

if isinstance(pobj, Reactive):
tgt_links = [Watcher(*l[:-3]) for l in pobj._links]
tgt_watchers = [w for w in get_watchers(pobj) if w not in pobj._callbacks
and w not in tgt_links and w not in param_pane._callbacks]
else:
tgt_watchers = []

for widget in param_pane._widgets.values():
if isinstance(widget, LiteralInput):
widget.serializer = 'json'

if (not pname or not isinstance(pobj, Reactive) or watchers or
pname[0] not in pobj._linkable_params):
pname[0] not in pobj._linkable_params or
(not isinstance(pobj, Widget) and tgt_watchers)):
return
return link_to_jslink(model, widget, 'value', pobj, pname[0])

Expand All @@ -97,13 +105,10 @@ def link_to_jslink(model, source, src_spec, target, tgt_spec):
declared forward and reverse JS transforms on the source and target.
"""
ref = model.ref['id']
tgt_links = [Watcher(*l[:-3]) for l in target._links]
tgt_watchers = [w for w in get_watchers(target) if w not in target._callbacks
and w not in tgt_links]

if ((source._source_transforms.get(src_spec, False) is None) or
(target._target_transforms.get(tgt_spec, False) is None) or
ref not in source._models or ref not in target._models or tgt_watchers):
ref not in source._models or ref not in target._models):
# We cannot jslink if either source or target declare
# that they apply Python transforms
return
Expand All @@ -116,18 +121,24 @@ def link_to_jslink(model, source, src_spec, target, tgt_spec):


def links_to_jslinks(model, widget):
from ..widgets import Widget

src_links = [Watcher(*l[:-3]) for l in widget._links]
if any(w not in widget._callbacks and w not in src_links for w in get_watchers(widget)):
return

links = []
for link in widget._links:
if link.transformed:
target = link.target
tgt_watchers = [w for w in get_watchers(target) if w not in target._callbacks]
if link.transformed or (tgt_watchers and not isinstance(target, Widget)):
return

mappings = []
for pname, tgt_spec in link.links.items():
if Watcher(*link[:-3]) in widget._param_watchers[pname]['value']:
mappings.append((pname, tgt_spec))

if mappings:
links.append((link, mappings))
jslinks = []
Expand Down Expand Up @@ -199,15 +210,12 @@ def embed_state(panel, model, doc, max_states=1000, max_opts=3,
# Replace parameter links with JS links
link = param_to_jslink(model, widget)
if link is not None:
pobj = widget._param_pane.object
if isinstance(pobj, Widget):
if not any(w not in pobj._callbacks and w not in widget._param_pane._callbacks
for w in get_watchers(pobj)):
ignore.append(pobj)
continue # Skip if we were able to attach JS link
pobj = widget._param_pane.object
if isinstance(pobj, Widget):
watchers = [w for w in get_watchers(pobj) if widget not in pobj._callbacks
and widget not in widget._param_pane._callbacks]
if not watchers:
# If underlying parameterized object is a widget
# which has no other links ensure it is skipped later
ignore.append(pobj)

if widget._links:
jslinks = links_to_jslinks(model, widget)
Expand Down Expand Up @@ -242,7 +250,7 @@ def embed_state(panel, model, doc, max_states=1000, max_opts=3,
add_to_doc(model, doc, True)
doc._held_events = []

if not values:
if not widget_data:
return

restore = [w.value for w, _, _, _ in values]
Expand Down
74 changes: 74 additions & 0 deletions panel/tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,55 @@
from panel.config import config
from panel.io.embed import embed_state
from panel.pane import Str
from panel.param import Param
from panel.widgets import Select, FloatSlider, Checkbox

from .util import jb_available


def test_embed_param_jslink(document, comm):
select = Select(options=['A', 'B', 'C'])
params = Param(select, parameters=['disabled']).layout
panel = Row(select, params)
with config.set(embed=True):
model = panel.get_root(document, comm)
embed_state(panel, model, document)
assert len(document.roots) == 1

ref = model.ref['id']
cbs = list(model.select({'type': CustomJS}))
assert len(cbs) == 2
cb1, cb2 = cbs
cb1, cb2 = (cb1, cb2) if select._models[ref][0] is cb1.args['target'] else (cb2, cb1)
assert cb1.code == """
value = source['active'];
value = value.indexOf(0) >= 0;
value = value;
try {
property = target.properties['disabled'];
if (property !== undefined) { property.validate(value); }
} catch(err) {
console.log('WARNING: Could not set disabled on target, raised error: ' + err);
return;
}
target['disabled'] = value;
"""

assert cb2.code == """
value = source['disabled'];
value = value;
value = value ? [0] : [];
try {
property = target.properties['active'];
if (property !== undefined) { property.validate(value); }
} catch(err) {
console.log('WARNING: Could not set active on target, raised error: ' + err);
return;
}
target['active'] = value;
"""


def test_embed_select_str_link(document, comm):
select = Select(options=['A', 'B', 'C'])
string = Str()
Expand All @@ -42,6 +86,36 @@ def link(target, event):
assert event['new'] == '<pre>%s</pre>' % k


def test_embed_select_str_link_two_steps(document, comm):
select = Select(options=['A', 'B', 'C'])
string1 = Str()
select.link(string1, value='object')
string2 = Str()
string1.link(string2, object='object')
panel = Row(select, string1, string2)
with config.set(embed=True):
model = panel.get_root(document, comm)
embed_state(panel, model, document)
_, state = document.roots
assert set(state.state) == {'A', 'B', 'C'}
for k, v in state.state.items():
content = json.loads(v['content'])
assert 'events' in content
events = content['events']
assert len(events) == 2
event = events[0]
assert event['kind'] == 'ModelChanged'
assert event['attr'] == 'text'
assert event['model'] == model.children[1].ref
assert event['new'] == '<pre>%s</pre>' % k

event = events[1]
assert event['kind'] == 'ModelChanged'
assert event['attr'] == 'text'
assert event['model'] == model.children[2].ref
assert event['new'] == '<pre>%s</pre>' % k


def test_embed_select_str_link_with_secondary_watch(document, comm):
select = Select(options=['A', 'B', 'C'])
string = Str()
Expand Down
55 changes: 54 additions & 1 deletion panel/tests/test_reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import param

from bokeh.models import Div
from panel.viewable import Reactive
from panel.layout import Tabs, WidgetBox
from panel.viewable import Layoutable, Reactive
from panel.widgets import Checkbox, StaticText, TextInput


def test_link():
Expand Down Expand Up @@ -76,3 +78,54 @@ class ReactiveLink(Reactive):
assert isinstance(cb, partial)
assert cb.args == (document, div.ref['id'])
assert cb.func == obj._server_change


def test_text_input_controls():
text_input = TextInput()

controls = text_input.controls()

assert isinstance(controls, Tabs)
assert len(controls) == 2
wb1, wb2 = controls
assert isinstance(wb1, WidgetBox)
assert len(wb1) == 4
name, disabled, value, placeholder = wb1

assert isinstance(name, StaticText)
assert isinstance(disabled, Checkbox)
assert isinstance(value, TextInput)
assert isinstance(placeholder, TextInput)

text_input.disabled = True
assert disabled.value

text_input.placeholder = "Test placeholder..."
assert placeholder.value == "Test placeholder..."

text_input.value = "New value"
assert value.value == "New value"

assert isinstance(wb2, WidgetBox)
assert len(wb2) == len(list(Layoutable.param)) + 1



def test_text_input_controls_explicit():
text_input = TextInput()

controls = text_input.controls(['placeholder', 'disabled'])

assert isinstance(controls, WidgetBox)
assert len(controls) == 3
name, disabled, placeholder = controls

assert isinstance(name, StaticText)
assert isinstance(disabled, Checkbox)
assert isinstance(placeholder, TextInput)

text_input.disabled = True
assert disabled.value

text_input.placeholder = "Test placeholder..."
assert placeholder.value == "Test placeholder..."
14 changes: 8 additions & 6 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,18 +886,17 @@ def controls(self, parameters=[], jslink=True):
from .widgets import LiteralInput

if parameters:
return Param(self.param, parameters=parameters, default_layout=WidgetBox,
name='Controls').layout

if jslink:
linkable = parameters
elif jslink:
linkable = self._linkable_params
else:
linkable = list(self.param)

params = [p for p in linkable if p not in Layoutable.param]
controls = Param(self.param, parameters=params, default_layout=WidgetBox,
name='Controls')
layout_params = [p for p in linkable if p in Layoutable.param]
if 'name' not in layout_params and self._rename.get('name', False) is not None:
if 'name' not in layout_params and self._rename.get('name', False) is not None and not parameters:
layout_params.insert(0, 'name')
style = Param(self.param, parameters=layout_params, default_layout=WidgetBox,
name='Layout')
Expand All @@ -912,8 +911,11 @@ def controls(self, parameters=[], jslink=True):
widget.jslink(self, value=p, bidirectional=True)
if isinstance(widget, LiteralInput):
widget.serializer = 'json'
if params:

if params and layout_params:
return Tabs(controls.layout[0], style.layout[0])
elif params:
return controls.layout[0]
return style.layout[0]

def link(self, target, callbacks=None, **links):
Expand Down

0 comments on commit ac7d924

Please sign in to comment.