import weakref
import bokeh
import bokeh.core.properties as bp
import param as pm
from bokeh.model import DataModel
from bokeh.models import ColumnDataSource
from ..reactive import Syncable
from .notebook import push_on_root
[docs]class Parameterized(bokeh.core.property.bases.Property):
""" Accept a Parameterized object.
This property only exists to support type validation, e.g. for "accepts"
clauses. It is not serializable itself, and is not useful to add to
Bokeh models directly.
"""
def validate(self, value, detail=True):
super().validate(value, detail)
if isinstance(value, pm.Parameterized):
return
msg = "" if not detail else f"expected param.Parameterized, got {value!r}"
raise ValueError(msg)
[docs]class ParameterizedList(bokeh.core.property.bases.Property):
""" Accept a list of Parameterized objects.
This property only exists to support type validation, e.g. for "accepts"
clauses. It is not serializable itself, and is not useful to add to
Bokeh models directly.
"""
[docs] def validate(self, value, detail=True):
super().validate(value, detail)
if isinstance(value, list) and all(isinstance(v, pm.Parameterized) for v in value):
return
msg = "" if not detail else f"expected list of param.Parameterized, got {value!r}"
raise ValueError(msg)
_DATA_MODELS = weakref.WeakKeyDictionary()
# The Bokeh Color property has `_default_help` set which causes
# an error to be raise when Nullable is called on it. This converter
# overrides the Bokeh _help to set it to None and avoid the error.
# See https://github.com/holoviz/panel/issues/3058
def color_param_to_ppt(p, kwargs):
ppt = bp.Color(**kwargs)
ppt._help = None
return ppt
def list_param_to_ppt(p, kwargs):
if isinstance(p.item_type, type) and issubclass(p.item_type, pm.Parameterized):
return bp.List(bp.Instance(DataModel)), [(ParameterizedList, lambda ps: [create_linked_datamodel(p) for p in ps])]
return bp.List(bp.Any, **kwargs)
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.ClassSelector: lambda p, kwargs: (
(bp.Instance(DataModel, **kwargs), [(Parameterized, create_linked_datamodel)])
if isinstance(p.class_, type) and issubclass(p.class_, pm.Parameterized) else
bp.Any(**kwargs)
),
pm.Color: color_param_to_ppt,
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: list_param_to_ppt,
pm.Number: lambda p, kwargs: bp.Float(**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 range(p.length)), **kwargs),
}
[docs]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)
[docs]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, pm.Parameterized):
cls = obj
elif isinstance(obj, pm.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)
properties = model.properties()
model = model(**{k: v for k, v in obj.param.values().items() if k in properties})
_changing = []
def cb_bokeh(attr, old, new):
if attr in _changing:
return
try:
_changing.append(attr)
obj.param.update(**{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)
tags = [tag for tag in model.tags if tag.startswith('__ref:')]
if root:
push_on_root(root.ref['id'])
elif tags:
ref = tags[0].split('__ref:')[-1]
push_on_root(ref)
finally:
for attr in update:
_changing.remove(attr)
for p in obj.param:
if p in properties:
model.on_change(p, cb_bokeh)
obj.param.watch(cb_param, list(set(properties) & set(obj.param)))
return model