Widget to show that function is 'running' in published Jupyter Notebook - python

In a published Jupyter notebook is there a way to insert a widget that says "running" or something similar when I am running a function.
I am aware of the tqdm function but to the best of my knowledge this is only when the function / process contains a for-loop.
I currently have a series of dropdown widgets with a submit button but some of the functions take a while for the calcs to run so i have no way of telling if the're running or not
Cheers

The way I have done this in the past is to have a function as a context manager, that displays some value to a Text widget to indicate that the function is running. You could also use an Output widget to display a indeterminate 'progress' bar like the one below:
https://www.iselect.com.au/content/themes/iselect/images/post/loader.gif
import ipywidgets as ipyw
import time
from contextlib import contextmanager
label = ipyw.Text('Ready')
button = ipyw.Button(description='Click me')
#contextmanager
def show_loading():
label.value = 'Running...'
yield
label.value = 'Ready'
def long_running_function(self):
with show_loading():
time.sleep(2)
button.on_click(long_running_function)
display(button)
display(label)

Related

Auto-refresh of prompt-toolkit fullscreen app

I'm trying to get a bit into prompt-toolkit for building CLI apps. Maybe it's a trivial question, but I could not find any solution in the docs or in other posts.
I want to build a full-screen app with prompt-toolkit, that monitors and displays periodically changing information. As a demonstrator, I just wanted to display the current date and time, and have it update every second.
However, I could not yet find any way for a prompt-toolkit app to update itself without user input. I assume, I need to add some callback somewhere, but the documentation is not very clear on that and I have not found a good example yet.
Update 1:
After some more trial&error I found that the following code produces the expected result, although I'm still not a 100% sure if this is the best way of doing it.
With app.create_background_task() one can add coroutines that to the event loop. In this example I update the text in the upper part of the window. However, this does not do anything, unless the user manually refreshes the app (I think with app.invalidate()) OR by providing the refresh argument in Application.
I think this solution is sufficient unless too much other stuff is happening so that the refresh-rate cannot be maintained..
import datetime
import asyncio
from prompt_toolkit import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import Window, HSplit
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit import widgets
static_text = FormattedTextControl(text=f"Time: {datetime.datetime.now().strftime('%H:%M:%S')}")
static_window = Window(content=static_text, height=2)
telemetry_window = Window(content=FormattedTextControl(text="Some fixed text"))
root_container = HSplit([
widgets.Frame(body=static_window),
widgets.Frame(body=telemetry_window)
])
layout = Layout(root_container)
kb = KeyBindings()
#kb.add('c-q')
#kb.add('c-c')
def exit_(event):
event.app.exit()
app = Application(layout=layout, full_screen=True, key_bindings=kb, refresh_interval=0.5)
async def refresh():
while True:
static_text.text = f"Time: {datetime.datetime.now().strftime('%H:%M:%S')}"
await asyncio.sleep(0.1)
app.create_background_task(refresh())
app.run()
Update 2:
It's also possible to update text controls through running threads. Every time the UI is invalidated, it renders itself with the new values set to text. So, a coroutine is not strictly necessary.
Documentation for Application shows:
refresh_interval – Automatically invalidate the UI every so many seconds.
When None (the default), only invalidate when invalidate has been called.
and it also shows:
on_invalidate – Called when the UI has been invalidated.
It automatically runs refresh when I assign it to on_invalidate=
def refresh(app):
#print(app)
static_text.text = f"Time: {datetime.datetime.now().strftime('%H:%M:%S')}"
app = Application(..., refresh_interval=0.5, on_invalidate=refresh)
or to before_render=
def refresh(app):
#print(app)
static_text.text = f"Time: {datetime.datetime.now().strftime('%H:%M:%S')}"
app = Application(..., refresh_interval=0.5, before_render=refresh)

ipywidgets progress for loop when pressing Button and change Button description

How to create a for loop that proceeds to the next item when a Button widget is pressed?
The Button description should be updated in the for loop.
In the official documentation of Asynchronous widgets, they show how to stop a for loop and then proceed to the next item when the user moves an IntSlider widget. It is mentioned that a similar result can be achieved with a Button, even though Buttons have a different interface than other widgets.
What I want is the same desired outcome of an existing question. Even though the question has an accepted answer, from the comments it is clear that the answer does not produce the desired outcome.
This code works, and it is based on the example from Asynchronous widgets documentation mentioned in the answer.
import asyncio
from IPython.display import display
import ipywidgets as widgets
out = widgets.Output()
def wait_for_click(btn, n_clicked=0):
future = asyncio.Future()
def on_button_clicked(btn):
btn.description = f"Clicked {n_clicked} times!"
future.set_result(btn.description)
btn.on_click(on_button_clicked)
return future
n_changes = 5
btn = widgets.Button(description="Never Clicked")
async def f():
for i in range(n_changes):
out.append_stdout(f'did work {i}\n')
btn_descr = await wait_for_click(btn, n_clicked=i + 1)
out.append_stdout(f'\nasync function continued with button description "{btn_descr}"\n')
asyncio.ensure_future(f())
display(btn, out)
The most important part is wait_for_click, a function that is called in each iteration of the for loop, and updates the Button description from "Never Clicked" to "Clicked N times!".
wait_for_click also returns the Button description as a string.
wait_for_click is a modification of wait_for_change from the documentation: instead of waiting for a modification of the IntSlider, it waits for a click of the Button.

How can I use async Python to allow Jupyter button widget clicks to process while running an async recursive function?

I set up a Jupyter Notebook where I'm running a recursive function that clears the output like so, creating an animated output:
from IPython.display import clear_output
active = True
def animate:
myOutput = ""
# do stuff
clear_output(wait=True)
print(myOutput)
sleep(0.2)
if active:
animate()
and that's working perfectly.
But now I want to add in one more step: A speed toggle. What I'm animating is a debugging visualization of a cursor moving through interpreted code as an interpreter I'm writing parses that code. I tried conditional slow-downs to have more time to read what's going on as the parsing continues, but what I really need is to be able to click a button to toggle the speed between fast and slow. Maybe I'll use a slider, but for now I just want a button for proof of concept.
This sounds simple enough. Note that I'm writing this statefully as a class because I need to read / write the state from within another imported class.
Jupyter block 1:
import ipywidgets as widgets
from IPython.display import display
out = widgets.Output()
class ToggleState():
def __init__(self):
self.button = widgets.Button(description="Toggle")
self.button.on_click(self.toggle)
display(self.button)
self.toggleState = False
print("Toggle State:", self.toggleState)
def toggle(self, arg): # arg has to be accepted here to meet on_click requirements
self.toggleState = not self.toggleState
print("Toggle State:", self.toggleState)
def read(self):
return self.toggleState
toggleState = ToggleState()
Then, in Jupyter block 2, note I decided to to do this in a separate block because the clear_output I'm doing with the animate func clears the button if it's in the same block, and therein lies the problem:
active = True
def animate:
myOutput = ""
# do stuff
clear_output(wait=True)
print(myOutput)
if toggleState.read():
sleep(5)
else:
sleep(0.2)
if active:
animate()
But the problem with this approach was that two blocks don't actually run at the same time (without using parallel kernels which is way more complexity than I care for) so that button can't keep receiving input in the previous block. Seems obvious now, but I didn't think about it.
How can I clear input in a way that doesn't delete my button too (so I can put the button in the animating block)?
Edit:
I thought I figured the solution, but only part of it:
Using the Output widget:
out = widgets.Output()
and
with out:
clear_output(wait=True) # clears only the logged output
# logging code
We can render to two separate stdouts within the same block. This works to an extent, as in the animation renders and the button isn't cleared. But still while the animation loop is running the button seems to be incapable of processing input. So it does seem like a synchronous code / event loop blocking problem. What's the issue here?
Do I need an alternative to sleep that frees up the event loop?
Edit 2:
After searching async code in Python, I learned about asyncio but I'm still struggling. Jupyter already runs the code via asyncio.run(), but the components obviously have to be defined as async for that to matter. I defined animate as async and tried using async sleeps, but the event loops still seems to be locked for the button.

Getting the current value of an IPython widget from within a loop

I'd like to get the values of the slider widgets within some loop. However, I would like these to update in real time.
For example see below. In the code pictured, I initialise some sliders, then run the loop below. Whilst the loop is running I move the sliders around. The printed values from the loop do not change from the initial value. When I execute the last cell, the updated slider values are shown.
Here's a solution inspired by the last example in asyncronous widgets (as suggested by #Fabio Perez):
import threading
from IPython.display import display
import ipywidgets as widgets
def f(a, b):
return
w = interactive(f, a=10, b=20)
def work(w):
for ii in range(10):
time.sleep(1)
print(w.kwargs, w.result)
thread = threading.Thread(target=work, args=(w,))
display(w)
thread.start()
This starts a separate thread which checks the values of the widgets.

Urwid ProgessBar does not update properly

I'm playing around with the urwid library and it's been pretty great so far.
But i can't get the Progressbar to work. I wrote a simple test program like this:
import os
import urwid
# a function that takes some times to complete
def dosomething(steps, bar):
bar.done = steps
for i in range(steps + 1):
bar.set_completion(i)
os.system('sleep 0.2')
# make a button to start
task_btn = urwid.Button(u'Task')
# progressbar object
pbar = urwid.ProgressBar('pg normal', 'pg complete')
# function called when the task button is clicked
def on_task_clicked(button):
dosomething(10, pbar)
# setup signal handler for the button
urwid.connect_signal(task_btn, 'click', on_task_clicked)
"""
create the interface and the mainloop.
filler objects are our friend to prevent unpacking errors :D
"""
loop = urwid.MainLoop(urwid.Pile([urwid.Filler(task_btn), urwid.Filler(pbar)]))
loop.run()
If i start it the progressbar is to 0% as it should be. Then i press the button, and a few seconds later the progressbar shows 100%. but i'm missing the steps between 0% and 100%. they just won't show up.
Also an additional call of the render function won't work.
I've also tried something like this:
def on_task_clicked(button):
pbar.set_completion(pbar.current+1)
And this works just fine. It just seems that the progressbar is not happy with being called in a loop. That seems strange?! Someone got any ideas in order to resolve this?
Thanks in advance :)
PS:
INFO:
urwid 1.2.0
tested on python 2.6.6, 2.7, 3.3 all the same
It's probably because the main loop's draw_screen method isn't being called (it's normally called automatically when the loop enters the idle state).
Add loop.draw_screen() inside your for-loop.

Categories

Resources