One of the things I needed in my projects was the (hopefully still under active development) “dynamic depends” (GH branch), so I’ve tried to implement it using param.watch and callbacks managing Watchers of the outer and inner (nested) Parameters. Just to be clear, by “dynamic depends” I mean being able to watch instance.param.nested_param and keep it working after instance.param’s value changes.
I ran into two problems.
First: Watchers’ fns get called as mentioned in the topic.
Second: Unwatching nested parameters doesn’t work (either by event.old.param.unwatch or watcher.inst.param.unwatch(watcher))
Below is simplified code presenting the first issue, that I’d like to focus on:
import inspect
import param as pm
R"""
1. add_info attribute to selected method using decorator notation
2. use_info attribute in Parameterized __init__ for each object with
added _info
- create watcher closure
- define nested event handler for watching/unwatching selected parameter
3. call event_handler once to set up initial watcher
4. change watched parameter's values
5. observe first callback fired twice???
_param_watchers hold reference to correct callbacks
event_handler closure contains correct references as well
"""
def add_info(param_attr_name):
"""Adds _info attribute to wrapped method.
No other side effects. Returns the same object.
"""
def get_fn_with_info(fn):
fn._info = dict(param_attr_name=param_attr_name)
return fn
return get_fn_with_info
def use_info(owner, callback, param_attr_name):
R"""Creates a watcher configured with _info attribute of wrapped method.
The wrapped method itself is to be used as a callback function for nested param changes
in dynamic depends scenario. Unused here.
"""
watcher = None
def event_handler(event=None):
nonlocal watcher
if event is None:
owner.message(f'Installing: {callback.__name__}')
else:
owner.message(f'Change event: {callback.__name__}')
if watcher is not None:
watcher.inst.param.unwatch(watcher)
watcher = owner.param.watch(event_handler, param_attr_name)
event_handler()
class C(pm.Parameterized):
watched_int = pm.Integer()
def __init__(self, **params):
super().__init__(**params)
for name, obj in inspect.getmembers(self):
info = getattr(obj, '_info', None)
if info is None:
continue
use_info(
owner=self,
callback=obj,
param_attr_name=info['param_attr_name'],
)
@add_info(param_attr_name='watched_int')
def callback_1(self, *events):
...
@add_info(param_attr_name='watched_int')
def callback_2(self, *events):
...
inst = C(name='inst', watched_int=0)
inst.watched_int = 1
inst.watched_int = 2
inst.watched_int = 3
Output:
INFO:param.inst: Installing: callback_1
INFO:param.inst: Installing: callback_2
INFO:param.inst: Change event: callback_1
INFO:param.inst: Change event: callback_1
INFO:param.inst: Change event: callback_2
INFO:param.inst: Change event: callback_2
INFO:param.inst: Change event: callback_1
INFO:param.inst: Change event: callback_1
Expected output:
INFO:param.inst: Installing: callback_1
INFO:param.inst: Installing: callback_2
INFO:param.inst: Change event: callback_1
INFO:param.inst: Change event: callback_2
INFO:param.inst: Change event: callback_1
INFO:param.inst: Change event: callback_2
INFO:param.inst: Change event: callback_1
INFO:param.inst: Change event: callback_2
I’m not certain it’s a bug and most likely just me not getting something right, so I’m asking here for help first.