0

I have a dashboard that gets node count, etc. from slurm and displays that as a button grid, and is refreshed every n seconds. I want the ability to click on any of the buttons and have something happen, e.g. get additional info, run a script, etc.

The problem is that interactivity with the buttons in the grid does not work in a while loop.

In the following test code, if event_loop = False, grid button clicks work as expected, but there is no update (no loop). If event_loop = True, the grid is updated, but the grid buttons no longer work.

from ipywidgets import GridspecLayout, Button, Layout, Output
from IPython.display import display, clear_output
import time

nodes = [1, 2, 3, 4, 5, 6]
rows = 3
cols = 3
event_loop = False

button_go = Button(
    description='Go',
    disabled=False,
    ayout={'width': '150px'}
)

output_command = Output()
output_command.append_stdout('')

@output_command.capture()
def button_go_clicked(event):
    print(event)
    if event_loop:
        while event_loop:
            update_grid(rows, cols, nodes)
            time.sleep(10)
    else:
        update_grid(rows, cols, nodes) 

button_go.on_click(button_go_clicked)

def create_dynamic_grid(rows, cols, nodes):
    grid = GridspecLayout(rows, cols)
    node = 0
    for i in range(rows):
        for j in range(cols):
            if node < len(nodes):
                if nodes[node]:
                    button_grid = Button(description='Node_{}-{}'.format(i, j), button_style='primary', layout=Layout(width='auto', height='auto'))
                
                grid[i, j] = button_grid
                grid[i, j].on_click(button_grid_clicked)

                node += 1
    return grid

output_grid = Output()

def update_grid(rows, cols, nodes):
    with output_grid:
         output_grid.clear_output()
         new_grid = create_dynamic_grid(rows, cols, nodes)
         display(new_grid)

@output_command.capture()
def button_grid_clicked(btn):
    print(btn)

display(button_go, 
        output_grid, 
        output_command)

I need some way to 1. immediately interrupt/pause the loop when clicking a grid button, and 2. maintain/resume the loop refresh time of n seconds.

After searching around on google, I found two interesting methods and integrated them. The first is from 'Kill a loop with Button Jupyter Notebook?'

The second was from a google Generative AI from the prompt 'python immediately break from sleep', which generated the following:

import time
import threading

def sleeper(seconds, event):
    while seconds > 0 and not event.is_set():
        time.sleep(min(seconds, 1))
        seconds -= 1
    if event.is_set():
      print("Sleep interrupted")
    else:
      print("Slept for", seconds, "seconds")

def main():
    sleep_event = threading.Event()
    sleep_thread = threading.Thread(target=sleeper, args=(5, sleep_event))
    sleep_thread.start()

    input("Press Enter to interrupt sleep...\n")
    sleep_event.set()
    sleep_thread.join()
    print("Continuing main thread")

if __name__ == "__main__":
    main() 

I have attempted to integrate these into my test code, my apologies if it's a bit of a jumble. The results are that when event_loop = True the grid is updated constantly, and if event_loop = False, no update.

from ipywidgets import GridspecLayout, Button, Output, Layout
from IPython.display import display, clear_output
import asyncio, threading, time

nodes = [1, 2, 3, 4, 5, 6]
rows = 3
cols = 3
event_loop = False
sleep_event = None
sleep_thread = None

button_go = Button(
    description='Go',
    disabled=False,
    layout={'width': '150px'}
)

output_command = Output()
output_command.append_stdout('')

async def function():
    while event_loop:
        print('function() event_loop: ', event_loop)
    print('done')
    
def sleeper(seconds, event):
    while seconds > 0 and not event.is_set():
        print('sleeping: ', event)
        print('sleep_time: ', seconds)
        time.sleep(seconds)
        if event.is_set():
            print('sleep interrupted: ', event)

def set_sleeper(sleep_time):
    global sleep_event
    global sleep_thread
    sleep_event = threading.Event()
    sleep_thread = threading.Thread(target=sleeper, args=(sleep_time, sleep_event))
    sleep_thread.start()
    return sleep_event

@output_command.capture()
def button_go_clicked(event):
    loop = asyncio.get_event_loop()    
    loop.create_task(function())
    print('button_go_clicked event_loop: ', event_loop)
    se = set_sleeper(10)
    print('set_sleeper: ', se)
    if event_loop:
        while event_loop:
            update_grid(rows, cols, nodes) # grid updates constantly in free run 
            # time.sleep(10) # <-- stop free run, but looking for the correct way to sleep here AND be able to interrupt on_click
    else:
        update_grid(rows, cols, nodes)

button_go.on_click(button_go_clicked)

def create_dynamic_grid(rows, cols, nodes):
    grid = GridspecLayout(rows, cols)
    node = 0
    for i in range(rows):
        for j in range(cols):
            if node < len(nodes):
                if nodes[node]:
                    button_grid = Button(description='Node_{}-{}'.format(i, j), button_style='primary', layout=Layout(width='auto', height='auto'))
                 
                grid[i, j] = button_grid
                grid[i, j].on_click(button_grid_clicked)

                node += 1
    return grid

output_grid = Output()

def update_grid(rows, cols, nodes):
    with output_grid:
         output_grid.clear_output()
         new_grid = create_dynamic_grid(rows, cols, nodes)
         display(new_grid)

@output_command.capture()
def button_grid_clicked(btn):
    print(btn)
    if event_loop:
        global sleep_event
        global sleep_thread
        print('interrupting sleep...\n')
        sleep_event.set()
        sleep_thread.join()
        print('continuing thread')

display(button_go,
        output_grid,
        output_command)

My anticipation was that set_sleeper(10) would act as the refresh time (10 seconds).

Any suggestions on how I can make this work as desired?

Regards

3
  • You sleeper function that you use in set_sleeper(10) is a block because it is using time.sleep(). You'd not want to use that, I think. I'd also suggest checking out 'how to abort a job using ipywidgets' and here and here. If this were provided as a minimal reproducible example I'd tackle it more but it seems overly complex at this stage. Commented Mar 28 at 16:06
  • @Wayne, thanks for the response. The problem isn't that the sleeper function is being blocked - it's the opposite; its a runway loop. Removing the sleep call in sleeper exacerbates that even further by making it two runaway loops, which is why its there in the first place. The links you provided are interesting and I'll explore them further, but I am not looking to abort the loop, simply pause it briefly then continue. Commented Mar 31 at 16:11
  • Sorry. Right. I thought when I first ran it, it seemed to block. But I didn't notice if I interrupted then it did run. So I was trying to track that and thought it may be it. Commented Mar 31 at 17:12

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.