Skip to content

Commit

Permalink
Fixes for jslinking HoloViews components (#3165)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Apr 4, 2022
1 parent cd32491 commit 340262d
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 56 deletions.
4 changes: 2 additions & 2 deletions panel/io/datamodel.py
Expand Up @@ -88,10 +88,10 @@ def list_param_to_ppt(p, kwargs):
pm.Integer: lambda p, kwargs: bp.Int(**kwargs),
pm.List: list_param_to_ppt,
pm.Number: lambda p, kwargs: bp.Float(**kwargs),
pm.NumericTuple: lambda p, kwargs: bp.Tuple(*(bp.Float for p in p.length), **kwargs),
pm.NumericTuple: lambda p, kwargs: bp.Tuple(*(bp.Float for p in range(p.length)), **kwargs),
pm.Range: lambda p, kwargs: bp.Tuple(bp.Float, bp.Float, **kwargs),
pm.String: lambda p, kwargs: bp.String(**kwargs),
pm.Tuple: lambda p, kwargs: bp.Tuple(*(bp.Any for p in p.length), **kwargs),
pm.Tuple: lambda p, kwargs: bp.Tuple(*(bp.Any for p in range(p.length)), **kwargs),
}


Expand Down
74 changes: 70 additions & 4 deletions panel/links.py
@@ -1,18 +1,78 @@
"""
Defines Links which allow declaring links between bokeh properties.
"""
import param
import weakref
import difflib
import sys
import weakref

import param

from bokeh.models import CustomJS, Model as BkModel
from bokeh.models import CustomJS, Model as BkModel, LayoutDOM

from .io.datamodel import create_linked_datamodel
from .models import ReactiveHTML
from .reactive import Reactive
from .viewable import Viewable


def assert_source_syncable(source, properties):
for prop in properties:
if prop.startswith('event:'):
continue
elif hasattr(source, 'object') and isinstance(source.object, LayoutDOM):
current = source.object
for attr in prop.split('.'):
if hasattr(current, attr):
current = getattr(current, attr)
continue
raise ValueError(
f"Could not resolve {prop} on {source.object} model. "
"Ensure you jslink an attribute that exists on the "
"bokeh model."
)
elif (prop not in source.param and prop not in list(source._rename.values())):
matches = difflib.get_close_matches(prop, list(source.param))
if matches:
matches = f' Similar parameters include: {matches!r}'
else:
matches = ''
raise ValueError(
f"Could not jslink {prop!r} parameter (or property) "
f"on {type(source).__name__} object because it was not "
"found.{matches}."
)
elif (source._source_transforms.get(prop, False) is None or
source._rename.get(prop, False) is None):
raise ValueError(
f"Cannot jslink {prop!r} parameter on {type(source).__name__} "
"object, the parameter requires a live Python kernel "
"to have an effect."
)

def assert_target_syncable(source, target, properties):
for k, p in properties.items():
if k.startswith('event:'):
continue
elif p not in target.param and p not in list(target._rename.values()):
matches = difflib.get_close_matches(p, list(target.param))
if matches:
matches = ' Similar parameters include: %r' % matches
else:
matches = ''
raise ValueError(
f"Could not jslink {p!r} parameter (or property) "
f"on {type(source).__name__} object because it was not "
"found. Similar parameters include: {matches}"
)
elif (target._source_transforms.get(p, False) is None or
target._rename.get(p, False) is None):
raise ValueError(
f"Cannot jslink {k!r} parameter on {type(source).__name__} "
f"object to {p!r} parameter on {type(target).__name__}. "
"It requires a live Python kernel to have an effect."
)


class Callback(param.Parameterized):
"""
A Callback defines some callback to be triggered when a property
Expand Down Expand Up @@ -102,7 +162,12 @@ def _process_callbacks(cls, root_view, root_model):

arg_overrides = {}
if 'holoviews' in sys.modules:
from holoviews.core.dimension import Dimensioned
from .pane.holoviews import HoloViews, generate_panel_bokeh_map
found = [
(link, src, tgt) for (link, src, tgt) in found
if not (isinstance(src, Dimensioned) or isinstance(tgt, Dimensioned))
]
hv_views = root_view.select(HoloViews)
map_hve_bk = generate_panel_bokeh_map(root_model, hv_views)
for src in linkable:
Expand Down Expand Up @@ -276,7 +341,8 @@ def _init_callback(self, root_model, link, source, src_spec, target, tgt_spec, c

src_model = self._resolve_model(root_model, source, src_spec[0])
ref = root_model.ref['id']
link_id = id(link)

link_id = (id(link), src_spec, tgt_spec)
if (any(link_id in cb.tags for cbs in src_model.js_property_callbacks.values() for cb in cbs) or
any(link_id in cb.tags for cbs in src_model.js_event_callbacks.values() for cb in cbs)):
# Skip registering callback if already registered
Expand Down
17 changes: 17 additions & 0 deletions panel/pane/holoviews.py
Expand Up @@ -335,6 +335,23 @@ def applies(cls, obj):
from holoviews.plotting.plot import Plot
return isinstance(obj, Dimensioned) or isinstance(obj, Plot)

def jslink(self, target, code=None, args=None, bidirectional=False, **links):
if links and code:
raise ValueError('Either supply a set of properties to '
'link as keywords or a set of JS code '
'callbacks, not both.')
elif not links and not code:
raise ValueError('Declare parameters to link or a set of '
'callbacks, neither was defined.')
if args is None:
args = {}

from ..links import Link
return Link(self, target, properties=links, code=code, args=args,
bidirectional=bidirectional)

jslink.__doc__ = PaneBase.jslink.__doc__

@classmethod
def widgets_from_dimensions(cls, object, widget_types=None, widgets_type='individual'):
from holoviews.core import Dimension, DynamicMap
Expand Down
51 changes: 3 additions & 48 deletions panel/reactive.py
Expand Up @@ -17,7 +17,6 @@
import numpy as np
import param

from bokeh.models import LayoutDOM
from bokeh.model import DataModel
from param.parameterized import ParameterizedMetaclass, Watcher

Expand Down Expand Up @@ -561,55 +560,11 @@ def jslink(self, target, code=None, args=None, bidirectional=False, **links):
if args is None:
args = {}

from .links import Link, assert_source_syncable, assert_target_syncable
mapping = code or links
for k in mapping:
if k.startswith('event:'):
continue
elif hasattr(self, 'object') and isinstance(self.object, LayoutDOM):
current = self.object
for attr in k.split('.'):
if not hasattr(current, attr):
raise ValueError(f"Could not resolve {k} on "
f"{self.object} model. Ensure "
"you jslink an attribute that "
"exists on the bokeh model.")
current = getattr(current, attr)
elif (k not in self.param and k not in list(self._rename.values())):
matches = difflib.get_close_matches(k, list(self.param))
if matches:
matches = ' Similar parameters include: %r' % matches
else:
matches = ''
raise ValueError("Could not jslink %r parameter (or property) "
"on %s object because it was not found.%s"
% (k, type(self).__name__, matches))
elif (self._source_transforms.get(k, False) is None or
self._rename.get(k, False) is None):
raise ValueError("Cannot jslink %r parameter on %s object, "
"the parameter requires a live Python kernel "
"to have an effect." % (k, type(self).__name__))

assert_source_syncable(self, mapping)
if isinstance(target, Syncable) and code is None:
for k, p in mapping.items():
if k.startswith('event:'):
continue
elif p not in target.param and p not in list(target._rename.values()):
matches = difflib.get_close_matches(p, list(target.param))
if matches:
matches = ' Similar parameters include: %r' % matches
else:
matches = ''
raise ValueError("Could not jslink %r parameter (or property) "
"on %s object because it was not found.%s"
% (p, type(self).__name__, matches))
elif (target._source_transforms.get(p, False) is None or
target._rename.get(p, False) is None):
raise ValueError("Cannot jslink %r parameter on %s object "
"to %r parameter on %s object. It requires "
"a live Python kernel to have an effect."
% (k, type(self).__name__, p, type(target).__name__))

from .links import Link
assert_target_syncable(self, target, mapping)
return Link(self, target, properties=links, code=code, args=args,
bidirectional=bidirectional)

Expand Down
3 changes: 1 addition & 2 deletions panel/tests/test_links.py
Expand Up @@ -82,8 +82,7 @@ def test_widget_link_no_target_transform_error():

with pytest.raises(ValueError) as excinfo:
t2.jslink(t1, value='value')
assert ("Cannot jslink \'value\' parameter on TextInput object "
"to \'value\' parameter on DatetimeInput object") in str(excinfo)
assert ("Cannot jslink 'value' parameter on TextInput object to 'value' parameter on DatetimeInput") in str(excinfo)


@hv_available
Expand Down

0 comments on commit 340262d

Please sign in to comment.