SOLVED: Updating Select widget options on new value gives ValueError

I am trying to make a simple navigation dashboard for a CSV file like below.

Screenshot 2024-03-06 153628

The data structure is essentially a tree and I want to navigate the nodes. The first Select widget is for parent nodes (usually 1, but can be more than 1), the middle Label widget is for current node value, and last Select widget is for children nodes.

The goal is when I select an option, I want to update all 3 widgets with new options (including the one I selected in). How can I force overwrite this.

Here is my current (not working) code, where I omitted the data code.

import panel as pn

pn.extension()

current_code = "root"

label_widget = pn.widgets.StaticText()
parent_select = pn.widgets.Select(size=10)
children_select = pn.widgets.Select(size=10)

Updating = False

dashboard = pn.Row(
    parent_select,
    label_widget,
    children_select
)

def set_parent_select(code):
    def get_parent_options(code):
        ...

    options = get_parent_options(code)

    if options == []:
        parent_select.disabled = True
    else:
        parent_select.options = options

def set_children_select(code):
    def get_children_options(code)
        ...

    options = get_children_options(code)
    
    if options == []:
        children_select.disabled = True
    else:
        children_select.options = options

def set_label_widget(event):
    global Updating
    if Updating:
        return

    Updating = True
    print(event.new)
    label_widget.value = event.new
    set_parent_select(event.new)
    set_children_select(event.new)
    Updating = False

parent_select.param.watch(set_label_widget, 'value')
children_select.param.watch(set_label_widget, 'value')

Updating = True
label_widget.value = current_code
set_parent_select(current_code)
set_children_select(current_code)
Updating = False

dashboard.servable()

When I select an option, I get the error ValueError: foobar not in list if I for example select foobar from any of the two Select widgets that have that option.

I didn’t look too closely, but I suggest using pn.bind and also param.parameterized.watch_batch_caller (or I forget the exact name, equivalent of the deprecated param.batch_watch() to update the options and value at the same time)

I managed to temporarily resolve the issue by replacing the Select widget with a new one, here is updated working code

import panel as pn

pn.extension()

placeholder_code = '---SELECT OPTION---'

def create_select():
    return pn.widgets.Select(size=10)

def create_parent_select(label_widget, code):
    def get_parent_options(code):
        ...

    options = get_parent_options(code)

    parent_select = create_select()
    parent_select.link(label_widget, callbacks={'value': set_label_widget})

    if options == []:
        parent_select.disabled = True
        parent_select.options = []
    else:
        parent_select.options = [placeholder_code] + options

    return parent_select

def create_children_select(label_widget, code):
    def get_children_options(code):
        ...

    children_select = create_select()
    children_select.link(label_widget, callbacks={'value': set_label_widget})

    options = get_children_options(code)

    if options == []:
        children_select.disabled = True
        children_select.options = []
    else:
        children_select.options =  [placeholder_code] + options

    return children_select

def create_dashboard(dashboard, starting_code="root"):
    if starting_code == placeholder_code:
        return

    dashboard.clear()

    label_widget = pn.widgets.StaticText(value=starting_code)
    parent_select = create_parent_select(label_widget, starting_code)
    children_select = create_children_select(label_widget, starting_code)

    dashboard.append(parent_select)
    dashboard.append(label_widget)
    dashboard.append(children_select)

    return dashboard

dashboard = pn.Row()

Updating = True
create_dashboard(dashboard)
Updating = False

def set_label_widget(target, event):
    global Updating
    global dashboard

    if Updating:
        return

    Updating = True
    create_dashboard(dashboard, event.new)
    Updating = False

dashboard.servable()

Essentially with each new option selection, I remove the old widget and create a new one with the updated options. I needed to append a place holder option, that is skipped on purpose, because otherwise it would right away callback again on the new selection of first element in widget upon creation.

For my purposes, the option lists are small and therefore it is acceptable solution for the time being.

1 Like