asyncio-glib
asyncio-glib copied to clipboard
Restart loop on demand
This will quit the GLib main loop in two events:
- A new task is created
- A Future finishes
See #1 for discussion on motivation for this. I think that those should be all reasons why the loop would need stopping from Python callbacks.
Note that there is one case left that is not covered -- if a Future is created the Old Way (as asyncio.Future(), not recommended any more), giving it a result will not give tasks blocked on it a chance to run until something else nudges the GLib main loop. This could be circumvented by monkey patching asyncio.Future to use our own future, but that's not really how things should be. We might want to offer an "install" function that sets the loop and replaces the Future class, but using that should be a conscious decision by the user and not just the way it's done so everything magically works.
Here's an extension of the demo in #1 that uses all the cases:
import asyncio
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk
import asyncio_glib
asyncio.set_event_loop_policy(asyncio_glib.GLibEventLoopPolicy())
async def do_something(win):
win.spinner.start()
await asyncio.sleep(5)
win.box.remove(win.spinner)
win.box.add(win.label)
win.show_all()
for i in range(10, 0, -1):
await asyncio.sleep(1)
win.label.props.label = "Closing in %d seconds" % i
class Window(Gtk.Window):
def __init__(self):
super().__init__()
self.box = Gtk.VBox()
b1 = Gtk.Button(label="Click me (this blocks)")
def b1_cb(event):
print("Blocking...")
import time
time.sleep(1)
print("Done")
b1.connect('clicked', b1_cb)
self.box.add(b1)
b2 = Gtk.Button(label="Click me (async)")
async def b2_cb(event):
b2.props.label = 'Please wait'
await asyncio.sleep(1)
# Creating another task to ensure things still work when tasks are created outside the main loop
asyncio.create_task(b2_part2())
async def b2_part2():
await asyncio.sleep(1)
b2.props.label = 'Click me again'
b2.connect('clicked', lambda event: asyncio.create_task(b2_cb(event)))
self.box.add(b2)
b22 = Gtk.Button(label="Click me (double async)")
async def b22_cb(event):
b22.props.label = 'Please wait'
await asyncio.sleep(1)
b22.props.label = 'Click me again'
# Create two racing tasks -- just to see that things still work when quit is called even though we're already quitting
b22.connect('clicked', lambda event: (asyncio.create_task(b22_cb(event)), asyncio.create_task(b22_cb(event))))
self.box.add(b22)
b3 = Gtk.Button(label="Click me (interrupt)")
def b3_cb(event):
import asyncio
loop = asyncio.get_event_loop()
loop._selector._main_loop.quit()
b3.connect('clicked', b3_cb)
self.box.add(b3)
# Will tasks that wait for a future continue once that future is set in a callback?
h = Gtk.HBox()
b = Gtk.Button(label="End old-style future")
flag = Gtk.Label(label="Not clicked yet")
future = asyncio.Future()
async def flagraiser(future=future, flag=flag):
result = await future
flag.props.label = "Clicked: %r" % result
asyncio.create_task(flagraiser())
b.connect("clicked", future.set_result)
h.add(b)
h.add(flag)
self.box.add(h)
# Will tasks that wait for a future continue once that future is set in a callback?
h = Gtk.HBox()
b = Gtk.Button(label="End new-style future")
flag = Gtk.Label(label="Not clicked yet")
future = asyncio.get_event_loop().create_future()
async def flagraiser(future=future, flag=flag):
result = await future
flag.props.label = "Clicked: %r" % result
asyncio.create_task(flagraiser())
b.connect("clicked", future.set_result)
h.add(b)
h.add(flag)
self.box.add(h)
self.spinner = Gtk.Spinner()
self.label = Gtk.Label(label="Done")
self.set_default_size(320, 320)
self.box.add(self.spinner)
self.add(self.box)
self.show_all()
async def main():
win = Window()
await do_something(win)
asyncio.run(main())
I should probably have read the other issues and not only #1 -- #3 does mention a few more entry points (call_soon etc) that also need to be covered. Nonetheless, I'm still confident that those entry points to the Python main loop can be enumerated, and all guarded thusly.