Search code examples
pythonjupyter-notebookipywidgets

how to abort a job using ipywidgets


I am creating a button that runs a job when clicked using ipywidgets all inside of a Jupyter Notebook. This job can take some long amount of time, so I would like to also give the user the ability to stop the job.

I've created a minimally reproducible example that runs for only 10 seconds. All of the following is run from a Jupyter Notebook cell:

import ipywidgets as widgets
from IPython.display import display
from time import sleep

button = widgets.Button(description='run job')
output = widgets.Output()


def abort(event):
    with output:
        print('abort!')

    
def run_job(event):
    with output:
        print('running job')
        button.description='abort'
        button.on_click(run_job, remove=True)
        button.on_click(abort)
        sleep(10)
        print('job complete!')

button.on_click(run_job)
display(button, output)

If the user clicks 'run job', then waits 2 seconds, then clicks 'abort'. The behavior is:

running job
job complete!
abort!

Which implies the 'abort' event fires after the job is already complete. I would like the print sequence to be:

running job
abort!
job complete!

If the abort can fire immediately when clicked, then I have a way to actually stop the job inside of my class objects.

How can I get the abort event to run immediately when clicked from a Jupyter Notebook?


Solution

  • After being pointed in the right direction by Wayne in the comment section, asyncio seems to provide the desired capability within Jupyter Notebooks and ipywidgets. In the following discourse thread bollwyvl posts a solution for how to use asyncio to accomplish a similar pattern https://discourse.jupyter.org/t/threading-with-matplotlib-and-ipywidgets/14674/2?u=fomightez

    import ipywidgets as widgets
    from IPython.display import display
    from time import sleep
    import asyncio
    
    button = widgets.Button(description='run job')
    tasks = dict()
    
    def abort(event):
        print('abort!')
        task = tasks.pop("run_job", None)
        if task:
            task.cancel()
    
    async def run_job():
        print('running job')
        button.description='abort'
        button.on_click(run_job, remove=True)
        button.on_click(abort)
        await asyncio.sleep(10)
        print('job complete!')
    
    def start_job(event):
        button.description='abort'
        button.on_click(start_job, remove=True)
        button.on_click(abort)
        tasks['run_job'] = asyncio.get_event_loop().create_task(run_job())
        print('started job')
    
    button.on_click(start_job)
    display(button)
    

    When 'start' button is clicked, then 'abort' button. The following output is produced:

    started job
    running job
    abort!