Dynamically update ObjectSelector dropdown

I have a need to programmatically add to a dropdown list. I’ve reduced my code down to this example:

class ObjSelect(param.Parameterized):
    rooms = param.ObjectSelector(default='room 1', objects=['room 1'])
    
    trigger = param.Integer(default=1)
    
    def on_click_btn(self, event):
        self.param.rooms.objects.append('room 2')
        self.rooms = 'room 2'
        self.trigger += 1
    
    @param.depends('trigger', watch=True)
    def panel(self):
        print('redraw')
        btn = pn.widgets.Button(name='click', width=50)
        btn.on_click(self.on_click_btn)
        
        return pn.Column(self.param.rooms, btn)
o = ObjSelect()
o.panel()

If you click on the button, it should add an object to the dropdown list.

If you check the objects after clicking the button, you can see that the object list has been updated, as expected:
o.param.rooms.objects

If you check the output print statement, you can see that it did trigger the redraw, but the widget didn’t change.

However, if you redraw the app entirely, you can see that it WAS added properly:
o.panel()

Why didn’t the programatic redraw update the widget? Is this the wrong way to update the ObjectSelector or is this a bug?

Hi @kcpevey!

I think you were pretty close to the solution! I just rewrote it in another way but you could adapt yours in the same way. The trick (it took me a while to understand it!) is that it seems that the widget is updated only if the object attribute of the rooms parameter gets a new list. In your case, appending to the objects attribute of the rooms parameter was actually not creating a list, as appending to a list is an operation that happens in place.

>>> x = [1]
>>> print(id(x))
139702400051632
>>> x.append(2)
139702400051632
>>> print(id(x))

The trick here was to force the objects attribute of the rooms parameter to get a new list to correctly update the widget options. I left some prints in there to help understanding what’s going on.

import param
import panel as pn
pn.extension()

class ObjSelect(param.Parameterized):
    rooms = param.ObjectSelector(default='room 1', objects=['room 1'])
    action = param.Action(lambda self: self.update_obj_list(), label='Update') 
    i = param.Integer(2, precedence=-2)

    def update_obj_list(self):
        print("update")
        l = self.param.rooms.objects.copy()
        new_obj = f"room {self.i}"
        l.append(new_obj)
        self.param.rooms.objects = l
        print(id(self.param.rooms.objects))
        self.rooms = new_obj
        self.i += 1
        print("updated")

        
o = ObjSelect()
app = pn.panel(o.param)
app.servable()

As for this is a bug, it looks like so but I’ll let @philippjfr decide if we need to fill an issue :wink:

1 Like

This is basically the same issue as this https://github.com/holoviz/panel/issues/459. The problem is that there is no way of knowing when a standard Python object has been modified. Bokeh solves this issue by subclassing these standard types to add listeners, which is a solution we could consider but the existing issue should be sufficient to cover that.

Thanks for the help! This is a straightforward workaround and it works great!

I see now that I had an additional problem in my original code. I ALSO needed to wrap the output in pn.panel(). Discussion on how we can better document this issue here: https://github.com/holoviz/panel/issues/1499

This doesn’t work with param.Parameterized objects to select. I get a lower level ValueError on row options (see below). Any idea what I’m doing wrong?

import param
import panel as pn
pn.extension()

class A(param.Parameterized):
    val = param.Integer()

class ObjSelect(param.Parameterized):
    a=A()
    rooms = param.ObjectSelector(default=a, objects=[a])
    action = param.Action(lambda self: self.update_obj_list(), label='Update') 
    i = param.Integer(2, precedence=-2)

    def update_obj_list(self):
        print("update")
        l = self.param.rooms.objects.copy()
        new_obj = A()
        l.append(new_obj)
        self.param.rooms.objects = l
        print(id(self.param.rooms.objects))
        self.rooms = new_obj
        self.i += 1
        print("updated")
        
o = ObjSelect()
pn.Param(o)

This throws an error when I try to update_obj_list.

Traceback (most recent call last):
  File "<myrootdir>\lib\site-packages\pyviz_comms\__init__.py", line 325, in _handle_msg
    self._on_msg(msg)
  File "<myrootdir>\lib\site-packages\panel\viewable.py", line 272, in _on_msg
    patch.apply_to_document(doc, comm.id)
  File "<myrootdir>\lib\site-packages\bokeh\protocol\messages\patch_doc.py", line 100, in apply_to_document
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "<myrootdir>\lib\site-packages\bokeh\document\document.py", line 1198, in _with_self_as_curdoc
    return f()
  File "<myrootdir>\lib\site-packages\bokeh\protocol\messages\patch_doc.py", line 100, in <lambda>
    doc._with_self_as_curdoc(lambda: doc.apply_json_patch(self.content, setter))
  File "<myrootdir>\lib\site-packages\bokeh\document\document.py", line 398, in apply_json_patch
    self._trigger_on_message(event_json["msg_type"], event_json["msg_data"])
  File "<myrootdir>\lib\site-packages\bokeh\document\document.py", line 687, in _trigger_on_message
    cb(msg_data)
  File "<myrootdir>\lib\site-packages\bokeh\document\document.py", line 356, in apply_json_event
    model._trigger_event(event)
  File "<myrootdir>\lib\site-packages\bokeh\util\callback_manager.py", line 91, in _trigger_event
    self._document._with_self_as_curdoc(invoke)
  File "<myrootdir>\lib\site-packages\bokeh\document\document.py", line 1198, in _with_self_as_curdoc
    return f()
  File "<myrootdir>\lib\site-packages\bokeh\util\callback_manager.py", line 80, in invoke
    callback(event)
  File "<myrootdir>\lib\site-packages\panel\widgets\button.py", line 119, in _server_click
    self._change_event(doc)
  File "<myrootdir>\lib\site-packages\panel\reactive.py", line 288, in _change_event
    self._process_events(events)
  File "<myrootdir>\lib\site-packages\panel\reactive.py", line 262, in _process_events
    self.param.set_param(**self_events)
  File "<myrootdir>\lib\site-packages\param\parameterized.py", line 1526, in set_param
    self_._batch_call_watchers()
  File "<myrootdir>\lib\site-packages\param\parameterized.py", line 1665, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "<myrootdir>\lib\site-packages\param\parameterized.py", line 1627, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "<myrootdir>\lib\site-packages\panel\param.py", line 448, in action
    value(self.object)
  File "C:\Users\MATS~1.VAN\AppData\Local\Temp/ipykernel_3568/2570219891.py", line 7, in <lambda>
    action = param.Action(lambda self: self.update_obj_list(), label='Update')
  File "C:\Users\MATS~1.VAN\AppData\Local\Temp/ipykernel_3568/2570219891.py", line 15, in update_obj_list
    self.param.rooms.objects = l
  File "<myrootdir>\lib\site-packages\param\parameterized.py", line 876, in __setattr__
    self.owner.param._call_watcher(watcher, event)
  File "<myrootdir>\lib\site-packages\param\parameterized.py", line 1645, in _call_watcher
    self_._execute_watcher(watcher, (event,))
  File "<myrootdir>\lib\site-packages\param\parameterized.py", line 1627, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "<myrootdir>\lib\site-packages\panel\param.py", line 522, in link
    widget.param.set_param(**updates)
  File "<myrootdir>\lib\site-packages\param\parameterized.py", line 1517, in set_param
    raise ValueError("'%s' is not a parameter of %s" % (k, self_or_cls.name))
ValueError: 'options' is not a parameter of Row04803
1 Like

I’m getting the same error in a similar situation.

The error traceback indicates that there is a problem with the pn.widgets.Select widget if your object list contains actual param.Parameterized objects.

In that case the Select widget contains 2 pieces encapsulated in a “Row”, the actual Selector + the nice little BLUE box […] to drill-down into the parameterized object selected.

when you update the self.param.rooms.object, this triggers the watchers that have been registered by the pn.widgets.Select to get informed of the change. But it can’t handle it correctly, because the top-level ‘Row’ doesn’t have an “options” parameter, only one of the widgets sub-objects, the selector has one.

So currently it looks like there is a bug when trying to update a selector list that contains parameterized-objects after the Select-widget has been instantiated. Or it’s just not supported ?

Anyway, well above my expertise level ,one of the usual suspects like @Marc or @philippjfr may be able to help.

Below some debugging:

I basically broke out the rooms-widget manually to be able to inspect it:

import param
import panel as pn
pn.extension()

class A(param.Parameterized):
    val = param.Integer()

class ObjSelect(param.Parameterized):
    a=A()
    rooms = param.ObjectSelector(default=a, objects=[a])
    action = param.Action(lambda self: self.update_obj_list(), label='Update') 
    i = param.Integer(2, precedence=-2)

    def update_obj_list(self):
        print("update")
        l = self.param.rooms.objects.copy()
        new_obj = A()
        l.append(new_obj)
        self.param.rooms.objects = l
        print(id(self.param.rooms.objects))
        self.rooms = new_obj
        self.i += 1
        print("updated")

        
o = ObjSelect()

w_rooms = pn.widgets.Select.from_param(o.param.rooms)
pn.Column(o.param.action, w_rooms)

The error message:

File "C:\Users\jogri\AppData\Local\Temp\ipykernel_868\2998770942.py", line 11, in <lambda>
    action = param.Action(lambda self: self.update_obj_list(), label='Update')
                                      ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jogri\AppData\Local\Temp\ipykernel_868\2998770942.py", line 19, in update_obj_list
    self.param.rooms.objects = l
    ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\site-packages\param\parameterized.py", line 1141, in __setattr__
    self.owner.param._call_watcher(watcher, event)
  File "C:\Python311\Lib\site-packages\param\parameterized.py", line 2043, in _call_watcher
    self_._execute_watcher(watcher, (event,))
  File "C:\Python311\Lib\site-packages\param\parameterized.py", line 2025, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "C:\Python311\Lib\site-packages\panel\param.py", line 594, in link
    widget.param.update(**updates)
  File "C:\Python311\Lib\site-packages\param\parameterized.py", line 1893, in update
    raise ValueError("'%s' is not a parameter of %s" % (k, self_or_cls.name))
ValueError: 'options' is not a parameter of Row02287

Now checking the watcher that gets actually triggered:

print(o.param.rooms.watchers)
{'constant': [Watcher(inst=ObjSelect(action=<function ObjSelect.<lambda> at 0x000001BEC3BA5580>, i=2, name='ObjSelect02270', rooms=A(name='A02269', val=0)), cls=<class '__main__.ObjSelect'>, fn=<function Param.widget.<locals>.link at 0x000001BEC3C5CB80>, mode='args', onlychanged=True, parameter_names=('rooms',), what='constant', queued=False, precedence=0)],
 'precedence': [Watcher(inst=ObjSelect(action=<function ObjSelect.<lambda> at 0x000001BEC3BA5580>, i=2, name='ObjSelect02270', rooms=A(name='A02269', val=0)), cls=<class '__main__.ObjSelect'>, fn=<function Param.widget.<locals>.link at 0x000001BEC3C5CB80>, mode='args', onlychanged=True, parameter_names=('rooms',), what='precedence', queued=False, precedence=0)],
 'label': [Watcher(inst=ObjSelect(action=<function ObjSelect.<lambda> at 0x000001BEC3BA5580>, i=2, name='ObjSelect02270', rooms=A(name='A02269', val=0)), cls=<class '__main__.ObjSelect'>, fn=<function Param.widget.<locals>.link at 0x000001BEC3C5CB80>, mode='args', onlychanged=True, parameter_names=('rooms',), what='label', queued=False, precedence=0)],
 'objects': [Watcher(inst=ObjSelect(action=<function ObjSelect.<lambda> at 0x000001BEC3BA5580>, i=2, name='ObjSelect02270', rooms=A(name='A02269', val=0)), cls=<class '__main__.ObjSelect'>, fn=<function Param.widget.<locals>.link at 0x000001BEC3C5CB80>, mode='args', onlychanged=True, parameter_names=('rooms',), what='objects', queued=False, precedence=0)]}

And here the widget details:

print(w_rooms)
display(w_rooms.param.values())
print(w_rooms.param.objects)
Row(width=300)
    [0] Select(margin=(5, 0, 5, 10), name='Rooms', options=OrderedDict([('A02269', ...]), sizing_mode='stretch_width', value=A)
    [1] Toggle(align='end', button_type='primary', height_policy='fit', margin=(0, 0, 5, 10), max_height=30, max_width=20, name='⋮')
{'align': 'start',
 'aspect_ratio': None,
 'background': None,
 'css_classes': [],
 'design': None,
 'height': None,
 'height_policy': 'auto',
 'loading': False,
 'margin': 0,
 'max_height': None,
 'max_width': None,
 'min_height': None,
 'min_width': None,
 'name': 'Row02287',
 'objects': [Select(margin=(5, 0, 5, 10), name='Rooms', options=OrderedDict([('A02269', ...]), sizing_mode='stretch_width', value=A),
  Toggle(align='end', button_type='primary', height_policy='fit', margin=(0, 0, 5, 10), max_height=30, max_width=20, name='⋮')],
 'scroll': False,
 'sizing_mode': None,
 'styles': {},
 'stylesheets': [],
 'tags': [],
 'visible': True,
 'width': 300,
 'width_policy': 'auto'}
<bound method Parameters.objects of <param.parameterized.Parameters object at 0x000001BEC3C79550>>