widgetjupyter-labtogglebutton

Display output going to log window instead of cell in Jupyter Lab


I have a jupyter lab script in which I want to run a for loop, and for each iteration I want to have some information displayed and then decide based on this information whether or not to execute some operation (create a symlink). Ideally, I would use a widget ToggleButtons by clicking on the 'Yes' option, or on the 'Skip' option in case I do not want to execute the operation. I think these details don't matter but please let me know in case you need to know more. The point is that the ToggleButtons widget I want to display in the cell output at each iteration is only displayed there for the first iteration. For the next iterations it goes to the log. How can I change that? If I cannot display the widget I cannot inform the script of my decisions. By looking at the log it is clear that I can skip to the next iterations til the end of the loop.

I have created an example that reproduces the problem:




from functools import wraps
import ipywidgets as widgets

def yield_for_button(widget, attribute):
#https://ipywidgets.readthedocs.io/en/7.6.5/examples/Widget%20Asynchronous.html    
    """Pause a generator to wait for a widget change event.

    This is a decorator for a generator function which pauses the generator on yield
    until the given widget attribute changes. The new value of the attribute is
    sent to the generator and is the value of the yield.
    """
    def f(iterator):
        @wraps(iterator)
        def inner():
            i = iterator()
            def next_i(change):
                try:
                    i.send(change.new)
                except StopIteration as e:
                    widget.unobserve(next_i, attribute)
            widget.observe(next_i, attribute)
            # start the generator
            next(i)
        return inner
    return f



#df_redcap = DefaultRedcapConnector().get_dataframe(raw_or_label="raw")
button=widgets.ToggleButton(
    value=False,
    description='Go to next!',
    disabled=False,
    button_style='', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Description',
    icon='check' # (FontAwesome names without the `fa-` prefix)
)

def on_button_clicked(*args,**kwargs):
    print("Button clicked.")

names = ['fabio', 'gaia', 'matteo', 'luca', 'eva']    
    
@yield_for_button(button, 'value')
def f():            
    for name in names:
        print(name)
        a = widgets.HTML(
            value = '<b>Like this name?</b><br>' ,
            placeholder='Some HTML',
            description='',
            disabled=True
        )

        c = widgets.ToggleButtons(
            options=['Yes', 'Skip', 'Stop'],
            value='Stop',
            description='Action to perform:'
        ) 

        z = widgets.VBox([a,c,button], layout=Layout(height='200px', overflow_y='auto'))

        display(z)


        action = yield
        #button.on_click(on_button_clicked)

        if c.value == 'Yes':
            print('I agree it s a nice name.')
        else:
            None
        #c.value = 'Stop'    


f()

My jupyter lab version is: 3.5.1 (I seem to remember that in a previous version the code ran without problems). This question is similar to this one, but I couldn't solve my problem by adapting it to my problem (i.e. putting the display within the with instruction).


Solution

  • This code block below gets closer to what you need to do by consistently wrapping the new widgets and print() statements in the widgets.Output() with a context manager as discussed in that question you reference and in the ipywidgets documentation here:

    from functools import wraps
    import ipywidgets as widgets
    
    output = widgets.Output()
    
    def yield_for_button(widget, attribute):
    #https://ipywidgets.readthedocs.io/en/7.6.5/examples/Widget%20Asynchronous.html    
        """Pause a generator to wait for a widget change event.
    
        This is a decorator for a generator function which pauses the generator on yield
        until the given widget attribute changes. The new value of the attribute is
        sent to the generator and is the value of the yield.
        """
        def f(iterator):
            @wraps(iterator)
            def inner():
                i = iterator()
                def next_i(change):
                    try:
                        i.send(change.new)
                    except StopIteration as e:
                        widget.unobserve(next_i, attribute)
                widget.observe(next_i, attribute)
                # start the generator
                next(i)
            return inner
        return f
    
    
    
    #df_redcap = DefaultRedcapConnector().get_dataframe(raw_or_label="raw")
    button=widgets.ToggleButton(
        value=False,
        description='Go to next!',
        disabled=False,
        button_style='', # 'success', 'info', 'warning', 'danger' or ''
        tooltip='Description',
        icon='check' # (FontAwesome names without the `fa-` prefix)
    )
    
    def on_button_clicked(*args,**kwargs):
        with output:
            print("Button clicked.")
    
    names = ['fabio', 'gaia', 'matteo', 'luca', 'eva']    
        
    @yield_for_button(button, 'value')
    def f():            
        for name in names:
            with output:
                print(name)
            a = widgets.HTML(
                value = '<b>Like this name?</b><br>' ,
                placeholder='Some HTML',
                description='',
                disabled=True
            )
    
            c = widgets.ToggleButtons(
                options=['Yes', 'Skip', 'Stop'],
                value='Stop',
                description='Action to perform:'
            ) 
    
            z = widgets.VBox([a,c,button], layout=widgets.Layout(height='200px', overflow_y='auto'))
    
            with output:
                display(z)
    
    
            action = yield
            #button.on_click(on_button_clicked)
    
            if c.value == 'Yes':
                with output:
                    print('I agree it s a nice name.')
            else:
                pass
            #c.value = 'Stop'
        return "test"
    
    
    f()
    output
    

    This works in both JupyterLab and Jupyter Notebook (NbClassic specifically) at present.

    Interestingly that didn't work completely in Jupyter Notebook 7.1 right now. The 'Go to next!' doesn't go to the next one.
    The example though at https://ipywidgets.readthedocs.io/en/7.6.5/examples/Widget%20Asynchronous.html works in Jupyter Notebook 7.1 if you hook the print() to output like this (note I had to change the display to have the slider and output:

    from functools import wraps
    import ipywidgets as widgets
    output = widgets.Output()
    def yield_for_change(widget, attribute):
        """Pause a generator to wait for a widget change event.
    
        This is a decorator for a generator function which pauses the generator on yield
        until the given widget attribute changes. The new value of the attribute is
        sent to the generator and is the value of the yield.
        """
        def f(iterator):
            @wraps(iterator)
            def inner():
                i = iterator()
                def next_i(change):
                    try:
                        i.send(change.new)
                    except StopIteration as e:
                        widget.unobserve(next_i, attribute)
                widget.observe(next_i, attribute)
                # start the generator
                next(i)
            return inner
        return f
    from ipywidgets import IntSlider, VBox, HTML
    slider2=IntSlider()
    
    @yield_for_change(slider2, 'value')
    def f():
        for i in range(10):
            with output:
                print('did work %s'%i)
            x = yield
            with output:
                print('generator function continued with value %s'%x)
    f()
    
    display(slider2,output)
    

    And so I adapted what I had from above to the following by adjusting what gets invoked on the last line for display():

    from functools import wraps
    import ipywidgets as widgets
    
    output = widgets.Output()
    
    def yield_for_button(widget, attribute):
    #https://ipywidgets.readthedocs.io/en/7.6.5/examples/Widget%20Asynchronous.html    
        """Pause a generator to wait for a widget change event.
    
        This is a decorator for a generator function which pauses the generator on yield
        until the given widget attribute changes. The new value of the attribute is
        sent to the generator and is the value of the yield.
        """
        def f(iterator):
            @wraps(iterator)
            def inner():
                i = iterator()
                def next_i(change):
                    try:
                        i.send(change.new)
                    except StopIteration as e:
                        widget.unobserve(next_i, attribute)
                widget.observe(next_i, attribute)
                # start the generator
                next(i)
            return inner
        return f
    
    
    
    #df_redcap = DefaultRedcapConnector().get_dataframe(raw_or_label="raw")
    button=widgets.ToggleButton(
        value=False,
        description='Go to next!',
        disabled=False,
        button_style='', # 'success', 'info', 'warning', 'danger' or ''
        tooltip='Description',
        icon='check' # (FontAwesome names without the `fa-` prefix)
    )
    
    def on_button_clicked(*args,**kwargs):
        with output:
            print("Button clicked.")
    
    names = ['fabio', 'gaia', 'matteo', 'luca', 'eva']    
        
    @yield_for_button(button, 'value')
    def f():            
        for name in names:
            with output:
                print(name)
            a = widgets.HTML(
                value = '<b>Like this name?</b><br>' ,
                placeholder='Some HTML',
                description='',
                disabled=True
            )
    
            c = widgets.ToggleButtons(
                options=['Yes', 'Skip', 'Stop'],
                value='Stop',
                description='Action to perform:'
            ) 
    
            z = widgets.VBox([a,c,button], layout=widgets.Layout(height='200px', overflow_y='auto'))
    
            with output:
                display(z)
    
    
            action = yield
            #button.on_click(on_button_clicked)
    
            if c.value == 'Yes':
                with output:
                    print('I agree it s a nice name.')
            else:
                pass
            #c.value = 'Stop'
        return "test"
    
    
    display(f(),output)
    

    But in JupyterLab that adds an extra 'None' before the expected things. I cannot quite figure out where it comes from.
    But it is closest to working universally so far.