From aadca5bdcbaaea963862d3998fbca13ec569a86d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 28 Jun 2021 13:49:58 +0200 Subject: [PATCH 1/2] Support jslinking Parameterized class --- panel/links.py | 147 +++++++++++++++++++++++++++++++++- panel/models/reactive_html.py | 55 +------------ panel/reactive.py | 4 +- 3 files changed, 149 insertions(+), 57 deletions(-) diff --git a/panel/links.py b/panel/links.py index 0ef7325299..0f165861e2 100644 --- a/panel/links.py +++ b/panel/links.py @@ -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): @@ -258,6 +399,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) diff --git a/panel/models/reactive_html.py b/panel/models/reactive_html.py index 103dbf7a09..0277eea17a 100644 --- a/panel/models/reactive_html.py +++ b/panel/models/reactive_html.py @@ -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 @@ -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): diff --git a/panel/reactive.py b/panel/reactive.py index 7e03f22015..5647ea8492 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -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 @@ -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__ From 5f4a16f0d054987425459687f0f2146243b2dd40 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 28 Jun 2021 14:06:33 +0200 Subject: [PATCH 2/2] Allow directly jslinking Parameterized --- panel/links.py | 1 + 1 file changed, 1 insertion(+) diff --git a/panel/links.py b/panel/links.py index 0f165861e2..f47bd46d73 100644 --- a/panel/links.py +++ b/panel/links.py @@ -236,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 = {}