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

Add ParamRef baseclass for ParamFunction and ParamMethod #6392

Merged
merged 8 commits into from
Feb 26, 2024
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
198 changes: 122 additions & 76 deletions panel/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

from param.parameterized import (
classlist, discard_events, eval_function_with_deps, get_method_owner,
iscoroutinefunction,
iscoroutinefunction, resolve_ref, resolve_value,
)
from param.reactive import rx

Expand Down Expand Up @@ -761,14 +761,11 @@ def select(self, selector=None):
return super().select(selector) + self.layout.select(selector)


class ParamMethod(ReplacementPane):
class ParamRef(ReplacementPane):
"""
ParamMethod panes wrap methods on parameterized classes and
rerenders the plot when any of the method's parameters change. By
default ParamMethod will watch all parameters on the class owning
the method or can be restricted to certain parameters by annotating
the method using the param.depends decorator. The method may
return any object which itself can be rendered as a Pane.
ParamRef wraps any valid parameter reference and resolves it
dynamically, re-rendering the output. If enabled it will attempt
to update the previously rendered component inplace.
"""

defer_load = param.Boolean(default=None, doc="""
Expand All @@ -786,6 +783,8 @@ class ParamMethod(ReplacementPane):
Whether to show a loading indicator while the pane is updating.
Can be set as parameter or by setting panel.config.loading_indicator.""")

priority: ClassVar[float | bool | None] = 0

def __init__(self, object=None, **params):
if 'defer_load' not in params:
params['defer_load'] = config.defer_load
Expand All @@ -800,30 +799,17 @@ def __init__(self, object=None, **params):
if not self.defer_load:
self._replace_pane()

@param.depends('object', watch=True)
def _validate_object(self):
dependencies = getattr(self.object, '_dinfo', None)
if not dependencies or not dependencies.get('watch'):
return
fn_type = 'method' if type(self) is ParamMethod else 'function'
self.param.warning(
f"The {fn_type} supplied for Panel to display was declared "
f"with `watch=True`, which will cause the {fn_type} to be "
"called twice for any change in a dependent Parameter. "
"`watch` should be False when Panel is responsible for "
f"displaying the result of the {fn_type} call, while "
f"`watch=True` should be reserved for {fn_type}s that work "
"via side-effects, e.g. by modifying internal state of a "
"class or global state in an application's namespace."
)
@classmethod
def applies(cls, obj: Any) -> float | bool | None:
return bool(resolve_ref(obj))

#----------------------------------------------------------------
# Callback API
#----------------------------------------------------------------

@classmethod
def eval(self, function):
return eval_function_with_deps(function)
def eval(cls, ref):
return resolve_value(ref)

async def _eval_async(self, awaitable):
if self._async_task:
Expand Down Expand Up @@ -886,7 +872,7 @@ def _replace_pane(self, *args, force=False):
def _update_pane(self, *events):
callbacks = []
for watcher in self._internal_callbacks:
obj = watcher.inst if watcher.inst is None else watcher.cls
obj = watcher.cls if watcher.inst is None else watcher.inst
if obj is self:
callbacks.append(watcher)
continue
Expand All @@ -895,6 +881,86 @@ def _update_pane(self, *events):
self._link_object_params()
self._replace_pane()

def _get_model(
self, doc: Document, root: Optional[Model] = None,
parent: Optional[Model] = None, comm: Optional[Comm] = None
) -> Model:
if not self._evaled:
deferred = self.defer_load and not state.loaded
if deferred:
state.onload(
partial(self._replace_pane, force=True),
threaded=bool(state._thread_pool)
)
self._replace_pane(force=not deferred)
return super()._get_model(doc, root, parent, comm)

def _link_object_params(self):
dep_params = resolve_ref(self.object)
if callable(self.object) and not (dep_params or self.lazy or self.defer_load or iscoroutinefunction(self.object)):
fn = getattr(self.object, '__bound_function__', self.object)
fn_name = getattr(fn, '__name__', repr(self.object))
self.param.warning(
f"The function {fn_name!r} does not have any dependencies "
"and will never update. Are you sure you did not intend "
"to depend on or bind a parameter or widget to this function? "
"If not simply call the function before passing it to Panel. "
"Otherwise, when passing a parameter as an argument, "
"ensure you pass at least one parameter and reference the "
"actual parameter object not the current value, i.e. use "
"object.param.parameter not object.parameter."
)
grouped = defaultdict(list)
for dep in dep_params:
grouped[id(dep.owner)].append(dep)
for group in grouped.values():
pobj = group[0].owner
watcher = pobj.param.watch(self._replace_pane, [dep.name for dep in group])
if isinstance(pobj, Reactive) and self.loading_indicator:
props = {dep.name: 'loading' for dep in group
if dep.name in pobj._linkable_params}
if props:
pobj.jslink(self._inner_layout, **props)
self._internal_callbacks.append(watcher)


@param.depends(config.param.defer_load, watch=True)
def _update_defer_load_default(default_value):
ParamRef.param.defer_load.default = default_value

@param.depends(config.param.loading_indicator, watch=True)
def _update_loading_indicator_default(default_value):
ParamRef.param.loading_indicator.default = default_value


class ParamMethod(ParamRef):
"""
ParamMethod panes wrap methods on parameterized classes and
rerenders the plot when any of the method's parameters change. By
default ParamMethod will watch all parameters on the class owning
the method or can be restricted to certain parameters by annotating
the method using the param.depends decorator. The method may
return any object which itself can be rendered as a Pane.
"""

priority: ClassVar[float | bool | None] = 0.5

@param.depends('object', watch=True)
def _validate_object(self):
dependencies = getattr(self.object, '_dinfo', {})
if not dependencies or not dependencies.get('watch'):
return
self.param.warning(
"The method supplied for Panel to display was declared "
"with `watch=True`, which will cause the method to be "
"called twice for any change in a dependent Parameter. "
"`watch` should be False when Panel is responsible for "
"displaying the result of the method call, while "
"`watch=True` should be reserved for methods that work "
"via side-effects, e.g. by modifying internal state of a "
"class or global state in an application's namespace."
)

def _link_object_params(self):
parameterized = get_method_owner(self.object)
params = parameterized.param.method_dependencies(self.object.__name__)
Expand Down Expand Up @@ -937,20 +1003,6 @@ def update_pane(*events):
watcher = pobj.param.watch(update_pane, ps, p.what)
self._internal_callbacks.append(watcher)

def _get_model(
self, doc: Document, root: Optional[Model] = None,
parent: Optional[Model] = None, comm: Optional[Comm] = None
) -> Model:
if not self._evaled:
deferred = self.defer_load and not state.loaded
if deferred:
state.onload(
partial(self._replace_pane, force=True),
threaded=bool(state._thread_pool)
)
self._replace_pane(force=not deferred)
return super()._get_model(doc, root, parent, comm)

#----------------------------------------------------------------
# Public API
#----------------------------------------------------------------
Expand All @@ -959,15 +1011,12 @@ def _get_model(
def applies(cls, obj: Any) -> float | bool | None:
return inspect.ismethod(obj) and isinstance(get_method_owner(obj), param.Parameterized)

@param.depends(config.param.defer_load, watch=True)
def _update_defer_load_default(default_value):
ParamMethod.param.defer_load.default = default_value
@classmethod
def eval(cls, ref):
return eval_function_with_deps(ref)

@param.depends(config.param.loading_indicator, watch=True)
def _update_loading_indicator_default(default_value):
ParamMethod.param.loading_indicator.default = default_value

class ParamFunction(ParamMethod):
class ParamFunction(ParamRef):
"""
ParamFunction panes wrap functions decorated with the param.depends
decorator and rerenders the output when any of the function's
Expand All @@ -980,34 +1029,21 @@ class ParamFunction(ParamMethod):

_applies_kw: ClassVar[bool] = True

def _link_object_params(self):
deps = getattr(self.object, '_dinfo', {})
dep_params = list(deps.get('dependencies', [])) + list(deps.get('kw', {}).values())
if not dep_params and not self.lazy and not self.defer_load and not iscoroutinefunction(self.object):
fn = getattr(self.object, '__bound_function__', self.object)
fn_name = getattr(fn, '__name__', repr(self.object))
self.param.warning(
f"The function {fn_name!r} does not have any dependencies "
"and will never update. Are you sure you did not intend "
"to depend on or bind a parameter or widget to this function? "
"If not simply call the function before passing it to Panel. "
"Otherwise, when passing a parameter as an argument, "
"ensure you pass at least one parameter and reference the "
"actual parameter object not the current value, i.e. use "
"object.param.parameter not object.parameter."
)
grouped = defaultdict(list)
for dep in dep_params:
grouped[id(dep.owner)].append(dep)
for group in grouped.values():
pobj = group[0].owner
watcher = pobj.param.watch(self._replace_pane, [dep.name for dep in group])
if isinstance(pobj, Reactive) and self.loading_indicator:
props = {dep.name: 'loading' for dep in group
if dep.name in pobj._linkable_params}
if props:
pobj.jslink(self._inner_layout, **props)
self._internal_callbacks.append(watcher)
@param.depends('object', watch=True)
def _validate_object(self):
dependencies = getattr(self.object, '_dinfo', {})
if not dependencies or not dependencies.get('watch'):
return
self.param.warning(
"The function supplied for Panel to display was declared "
"with `watch=True`, which will cause the function to be "
"called twice for any change in a dependent Parameter. "
"`watch` should be False when Panel is responsible for "
"displaying the result of the function call, while "
"`watch=True` should be reserved for functions that work "
"via side-effects, e.g. by modifying internal state of a "
"class or global state in an application's namespace."
)

#----------------------------------------------------------------
# Public API
Expand All @@ -1027,6 +1063,10 @@ def applies(cls, obj: Any, **kwargs) -> float | bool | None:
return None
return False

@classmethod
def eval(self, ref):
return eval_function_with_deps(ref)


class ReactiveExpr(PaneBase):
"""
Expand Down Expand Up @@ -1158,6 +1198,12 @@ def widgets(self):
widgets.append(w)
return self.widget_layout(*widgets)

def _get_model(
self, doc: Document, root: Optional['Model'] = None,
parent: Optional['Model'] = None, comm: Optional[Comm] = None
) -> 'Model':
return self.layout._get_model(doc, root, parent, comm)

def _generate_layout(self):
panel = ParamFunction(self.object._callback)
if not self.show_widgets:
Expand Down
9 changes: 6 additions & 3 deletions panel/tests/pane/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
Bokeh, HoloViews, Interactive, IPyWidget, Markdown, PaneBase, RGGPlot,
Vega,
)
from panel.param import Param, ParamMethod, ReactiveExpr
from panel.param import (
Param, ParamFunction, ParamMethod, ParamRef, ReactiveExpr,
)
from panel.tests.util import check_layoutable_properties
from panel.util import param_watchers

SKIP_PANES = (
Bokeh, HoloViews, Interactive, IPyWidget, Param, ParamMethod, RGGPlot,
ReactiveExpr, Vega, interactive, ChatMessage
Bokeh, ChatMessage, HoloViews, Interactive, IPyWidget, Param,
ParamFunction, ParamMethod, ParamRef, RGGPlot, ReactiveExpr, Vega,
interactive
)

all_panes = [w for w in param.concrete_descendents(PaneBase).values()
Expand Down
Loading