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:

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