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

Support jslinking Parameterized class #2441

Merged
merged 2 commits into from
Jun 29, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 146 additions & 2 deletions panel/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,153 @@
import weakref
import sys

import bokeh.core.properties as bp
import param as pm

from bokeh.model import DataModel
from bokeh.models import ColumnDataSource, CustomJS, Model as BkModel

from .config import config
from .io.notebook import push_on_root
from .models import ReactiveHTML
from .reactive import Reactive
from .reactive import Reactive, Syncable
from .viewable import Viewable

from bokeh.models import CustomJS, Model as BkModel

_DATA_MODELS = weakref.WeakKeyDictionary()

PARAM_MAPPING = {
pm.Array: lambda p, kwargs: bp.Array(bp.Any, **kwargs),
pm.Boolean: lambda p, kwargs: bp.Bool(**kwargs),
pm.CalendarDate: lambda p, kwargs: bp.Date(**kwargs),
pm.CalendarDateRange: lambda p, kwargs: bp.Tuple(bp.Date, bp.Date, **kwargs),
pm.Color: lambda p, kwargs: bp.Color(**kwargs),
pm.DataFrame: lambda p, kwargs: (
bp.ColumnData(bp.Any, bp.Seq(bp.Any), **kwargs),
[(bp.PandasDataFrame, lambda x: ColumnDataSource._data_from_df(x))]
),
pm.DateRange: lambda p, kwargs: bp.Tuple(bp.Datetime, bp.Datetime, **kwargs),
pm.Date: lambda p, kwargs: bp.Datetime(**kwargs),
pm.Dict: lambda p, kwargs: bp.Dict(bp.String, bp.Any, **kwargs),
pm.Event: lambda p, kwargs: bp.Bool(**kwargs),
pm.Integer: lambda p, kwargs: bp.Int(**kwargs),
pm.List: lambda p, kwargs: bp.List(bp.Any, **kwargs),
pm.Number: lambda p, kwargs: bp.Float(**kwargs),
pm.NumericTuple: lambda p, kwargs: bp.Tuple(*(bp.Float for p in 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),
}


def construct_data_model(parameterized, name=None, ignore=[], types={}):
"""
Dynamically creates a Bokeh DataModel class from a Parameterized
object.

Arguments
---------
parameterized: param.Parameterized
The Parameterized class or instance from which to create the
DataModel
name: str or None
Name of the dynamically created DataModel class
ignore: list(str)
List of parameters to ignore.
types: dict
A dictionary mapping from parameter name to a Parameter type,
making it possible to override the default parameter types.

Returns
-------
DataModel
"""

properties = {}
for pname in parameterized.param:
if pname in ignore:
continue
p = parameterized.param[pname]
if p.precedence and p.precedence < 0:
continue
ptype = types.get(pname, type(p))
prop = PARAM_MAPPING.get(ptype)
if isinstance(parameterized, Syncable):
pname = parameterized._rename.get(pname, pname)
if pname == 'name' or pname is None:
continue
nullable = getattr(p, 'allow_None', False)
kwargs = {'default': p.default, 'help': p.doc}
if prop is None:
bk_prop, accepts = bp.Any(**kwargs), []
else:
bkp = prop(p, {} if nullable else kwargs)
bk_prop, accepts = bkp if isinstance(bkp, tuple) else (bkp, [])
if nullable:
bk_prop = bp.Nullable(bk_prop, **kwargs)
for bkp, convert in accepts:
bk_prop = bk_prop.accepts(bkp, convert)
properties[pname] = bk_prop
name = name or parameterized.name
return type(name, (DataModel,), properties)


def create_linked_datamodel(obj, root=None):
"""
Creates a Bokeh DataModel from a Parameterized class or instance
which automatically links the parameters bi-directionally.

Arguments
---------
obj: param.Parameterized
The Parameterized class to create a linked DataModel for.

Returns
-------
DataModel instance linked to the Parameterized object.
"""
if isinstance(obj, type) and issubclass(obj, param.Parameterized):
cls = obj
elif isinstance(obj, param.Parameterized):
cls = type(obj)
else:
raise TypeError('Can only create DataModel for Parameterized class or instance.')
if cls in _DATA_MODELS:
model = _DATA_MODELS[cls]
else:
_DATA_MODELS[cls] = model = construct_data_model(obj)
model = model(**dict(obj.param.get_param_values()))
_changing = []

def cb_bokeh(attr, old, new):
if attr in _changing:
return
try:
_changing.append(attr)
obj.param.set_param(**{attr: new})
finally:
_changing.remove(attr)

def cb_param(*events):
update = {
event.name: event.new for event in events
if event.name not in _changing
}
try:
_changing.extend(list(update))
model.update(**update)
if root:
push_on_root(root.ref['id'])
finally:
for attr in update:
_changing.remove(attr)

for p in obj.param:
model.on_change(p, cb_bokeh)

obj.param.watch(cb_param, list(obj.param))

return model


class Callback(param.Parameterized):
Expand Down Expand Up @@ -95,6 +236,7 @@ def _process_callbacks(cls, root_view, root_model):
(link, src, getattr(link, 'target', None)) for src in linkable
for link in cls.registry.get(src, [])
if not link._requires_target or link.target in linkable
or isinstance(link.target, param.Parameterized)
]

arg_overrides = {}
Expand Down Expand Up @@ -258,6 +400,8 @@ def _resolve_model(cls, root_model, obj, model_spec):
model, _ = obj._models[root_model.ref['id']]
elif isinstance(obj, BkModel):
model = obj
elif isinstance(obj, param.Parameterized):
model = create_linked_datamodel(obj, root_model)
if model_spec is not None:
for spec in model_spec.split('.'):
model = getattr(model, spec)
Expand Down
55 changes: 1 addition & 54 deletions panel/models/reactive_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from html.parser import HTMLParser

import bokeh.core.properties as bp
import param as pm

from bokeh.models import ColumnDataSource, HTMLBox, LayoutDOM
from bokeh.models import HTMLBox, LayoutDOM
from bokeh.model import DataModel
from bokeh.events import ModelEvent

Expand Down Expand Up @@ -140,58 +139,6 @@ def find_attrs(html):
return p.attrs


PARAM_MAPPING = {
pm.Array: lambda p, kwargs: bp.Array(bp.Any, **kwargs),
pm.Boolean: lambda p, kwargs: bp.Bool(**kwargs),
pm.CalendarDate: lambda p, kwargs: bp.Date(**kwargs),
pm.CalendarDateRange: lambda p, kwargs: bp.Tuple(bp.Date, bp.Date, **kwargs),
pm.Color: lambda p, kwargs: bp.Color(**kwargs),
pm.DataFrame: lambda p, kwargs: (
bp.ColumnData(bp.Any, bp.Seq(bp.Any), **kwargs),
[(bp.PandasDataFrame, lambda x: ColumnDataSource._data_from_df(x))]
),
pm.DateRange: lambda p, kwargs: bp.Tuple(bp.Datetime, bp.Datetime, **kwargs),
pm.Date: lambda p, kwargs: bp.Datetime(**kwargs),
pm.Dict: lambda p, kwargs: bp.Dict(bp.String, bp.Any, **kwargs),
pm.Event: lambda p, kwargs: bp.Bool(**kwargs),
pm.Integer: lambda p, kwargs: bp.Int(**kwargs),
pm.List: lambda p, kwargs: bp.List(bp.Any, **kwargs),
pm.Number: lambda p, kwargs: bp.Float(**kwargs),
pm.NumericTuple: lambda p, kwargs: bp.Tuple(*(bp.Float for p in 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),
}


def construct_data_model(parameterized, name=None, ignore=[], types={}):
properties = {}
for pname in parameterized.param:
if pname in ignore:
continue
p = parameterized.param[pname]
if p.precedence and p.precedence < 0:
continue
ptype = types.get(pname, type(p))
prop = PARAM_MAPPING.get(ptype)
pname = parameterized._rename.get(pname, pname)
if pname == 'name' or pname is None:
continue
nullable = getattr(p, 'allow_None', False)
kwargs = {'default': p.default, 'help': p.doc}
if prop is None:
bk_prop, accepts = bp.Any(**kwargs), []
else:
bkp = prop(p, {} if nullable else kwargs)
bk_prop, accepts = bkp if isinstance(bkp, tuple) else (bkp, [])
if nullable:
bk_prop = bp.Nullable(bk_prop, **kwargs)
for bkp, convert in accepts:
bk_prop = bk_prop.accepts(bkp, convert)
properties[pname] = bk_prop
name = name or parameterized.name
return type(name, (DataModel,), properties)


class DOMEvent(ModelEvent):

Expand Down
4 changes: 3 additions & 1 deletion panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from .io.server import unlocked
from .io.state import state
from .models.reactive_html import (
ReactiveHTML as _BkReactiveHTML, ReactiveHTMLParser, construct_data_model
ReactiveHTML as _BkReactiveHTML, ReactiveHTMLParser
)
from .util import edit_readonly, escape, updating
from .viewable import Layoutable, Renderable, Viewable
Expand Down Expand Up @@ -936,6 +936,8 @@ class ReactiveHTMLMetaclass(ParameterizedMetaclass):
_script_regex = r"script\([\"|'](.*)[\"|']\)"

def __init__(mcs, name, bases, dict_):
from .links import construct_data_model

mcs.__original_doc__ = mcs.__doc__
ParameterizedMetaclass.__init__(mcs, name, bases, dict_)
cls_name = mcs.__name__
Expand Down