10

I am using widgets in IPython that allows the user to repeatedly search for a phrase and see the results (different titles) in another widget (a selection widget) and then select one of the results.

In short:

search_text = widgets.Text(description = 'Search') 
search_result = widgets.Select(description = 'Select table')

def search_action(sender):
    phrase = search_text.value
    df = search(phrase) # A function that returns the results in a pandas df
    titles = df['title'].tolist()
    search_result.options = titles

search_text.on_submit(search_action)

This used to work fine, but after updating to the latest version of ipywidgets (5.1.3 from 4.0.1) it seems like

search_selection.options = titles

Produce the following errors (one or both, it varies):

TraitError: Invalid selection
TypeError: 'list' object is not callable

It still works in the sense that the widget gets updated with the results based on the search from the other widget, but it gives an error.

What is the correct way of setting the options in one widget based on the results from another widget?

(edit: added more detailed error message)

hmelberg
  • 307
  • 1
  • 3
  • 8

4 Answers4

6

I encountered this exact problem an hour ago. I have hacked together a solution using the minimum example here: Dynamically changing dropdowns in IPython notebook widgets and Spyre, since my own requirements were to have dynamically linked lists. I am sure you'll be able to adapt your requirements using this solution.

The key is to pre-generate all Dropdowns/Select. For some reason, w.options = l only sets w._options_labels but not w.options. Subsequent validation of the selected value of w will then fail horribly.

import ipywidgets as widgets
from IPython.display import display

geo={'USA':['CHI','NYC'],'Russia':['MOW','LED']}
geoWs = {key: widgets.Select(options=geo[key]) for key in geo}

def get_current_state():
    return {'country': i.children[0].value,
            'city': i.children[1].value}

def print_city(**func_kwargs):
    print('func_kwargs', func_kwargs)
    print('i.kwargs', i.kwargs)
    print('get_current_state', get_current_state())

def select_country(country):
    new_i = widgets.interactive(print_city, country=countryW, city=geoWs[country['new']])
    i.children = new_i.children

countryW = widgets.Select(options=list(geo.keys()))
init = countryW.value
cityW = geoWs[init]

countryW.observe(select_country, 'value')

i = widgets.interactive(print_city, country=countryW, city=cityW)

display(i)

Note lastly that it is not trivial to obtain the most up-to-date state of the widgets. These are

  • directly from the children's values, via get_current_state. This can be trusted.
  • from the interactive instance, via i.kwargs
  • from the supplied args to print_city

The latter two can sometimes be out of date, for various reasons I don't wish to find out further.

Hope this helps.

Community
  • 1
  • 1
ch41rmn
  • 121
  • 3
5

You can hold notifications for during the assignment to options:

with search_result.hold_trait_notifications():
    search_result.options = titles

Thus:

search_text = widgets.Text(description = 'Search') 
search_result = widgets.Select(description = 'Select table')

def search_action(sender):
    phrase = search_text.value
    df = search(phrase) # A function that returns the results in a pandas df
    titles = df['title'].tolist()
    with search_result.hold_trait_notifications():
        search_result.options = titles

See hmelberg's explanation below

"The root of the error is that the widget also has a value property and the value may not be in the new list of options. Because of this, widget value may be "orphaned" for a short time and an error is produced."

Dan Schien
  • 1,392
  • 1
  • 17
  • 29
2

I had a similar problem and solved it Registering callbacks to trait changes in the kernel

caption = widgets.Label(value='The values of range1 and range2 are synchronized')
slider = widgets.IntSlider(min=-5, max=5, value=1, description='Slider')

def handle_slider_change(change):
    caption.value = 'The slider value is ' + (
        'negative' if change.new < 0 else 'nonnegative'
    )

slider.observe(handle_slider_change, names='value')

display(caption, slider)

I guess this solution wasn't available back in 2016 or my problem wasn't as similar as thought.

Ruy Díaz
  • 21
  • 4
0

The root of the error is that the widget also has a value property and the value may not be in the new list of options. Because of this, widget value may be "orphaned" for a short time and an error is produced.

The solution is either to assign the widget value to the option list before assigning it to the widget (and remove the value/option after if desired), or as Dan writes: use create a hold_trait-notifications()

Dan's approach is the best. The above just explains the cause of the problem.

hmelberg
  • 307
  • 1
  • 3
  • 8