asyncio-glib
asyncio-glib copied to clipboard
GTK example
Could you include an example of how you would use this with a GTK application?
For example, convert the below program into asyncio, while replacing the time.sleep() with an await so that the window still runs until the timeout:
import time
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk
class Application(Gtk.Application):
def do_activate(self):
self.window = Gtk.ApplicationWindow(application=self)
spinner = Gtk.Spinner()
label = Gtk.Label("Done")
self.window.set_default_size(320, 320)
self.window.add(spinner)
self.window.show_all()
spinner.start()
time.sleep(10)
self.window.remove(spinner)
self.window.add(label)
self.window.show_all()
app = Application()
app.run()
For comparison, this works with gbulb:
import asyncio
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk
import gbulb
gbulb.install(gtk=True)
async def do_something(app):
app.spinner.start()
await asyncio.sleep(10)
app.window.remove(app.spinner)
app.window.add(app.label)
app.window.show_all()
class Application(Gtk.Application):
def do_activate(self):
self.window = Gtk.ApplicationWindow(application=self)
self.spinner = Gtk.Spinner()
self.label = Gtk.Label("Done")
self.window.set_default_size(320, 320)
self.window.add(self.spinner)
self.window.show_all()
asyncio.ensure_future(do_something(self))
app = Application()
loop = asyncio.get_event_loop()
loop.run_forever(application=app)
There are two main pain points for getting an example like this working with asyncio-glib as it currently stands:
-
asyncio-glib relies on the main context being iterated through the asyncio event loop. This is so we can stop iterating the GLib loop when there are asyncio tasks or timers to invoke.
-
GLib callbacks happen outside of the view of the asyncio loop. This was done so that Python code wouldn't be on the critical path when the event loop is being used for non-asyncio tasks. But things like
ensure_futurerely on work done when the asyncio event loop is iterated.
I don't have a good solution for (1). For (2) though, we can use the existing API for scheduling work from other threads. Something like this:
loop.call_soon_threadsafe(asyncio.ensure_future, do_something(self))
I'm floundering around trying to get asyncio-glib to work with Gtk (even just a simple 'hello world' example like this one), and would also appreciate a working example so I can wrap my head around how this library is supposed to work with Gtk.
The closest I have gotten is something like this:
import asyncio
import asyncio_glib
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
asyncio.set_event_loop_policy(asyncio_glib.GLibEventLoopPolicy())
async def foo():
await asyncio.sleep(2)
print('foo done')
async def bar():
win = Gtk.Window()
button = Gtk.Button()
win.add(button)
win.connect('destroy', Gtk.main_quit)
win.show()
win.show_all()
loop = asyncio.get_event_loop()
loop.call_soon_threadsafe(asyncio.ensure_future, foo())
loop.call_soon_threadsafe(asyncio.ensure_future, bar())
loop.run_forever()
But I'm not sure that this is the intended way to use asyncio, asyncio-glib, and Gtk together (the example has a number of issues).
I'm not good with Gtk.Application, but I think this could serve as a nice first example:
import asyncio
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import 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.remove(win.spinner)
win.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.spinner = Gtk.Spinner()
self.label = Gtk.Label(label="Done")
self.set_default_size(320, 320)
self.add(self.spinner)
self.show_all()
async def main():
win = Window()
await do_something(win)
asyncio.run(main())
(being comparable to the gbulb example but using the modern asyncio idiom of asyncio.run()).
- GLib callbacks happen outside of the view of the asyncio loop
We may not need to do work around here but can enumerate the cases in which the loop needs to break:
- A new task is created
- A task becomes runnable because a future it waited for has been set to completed.
Anything else? The custom loop should be able to do both of those.
I've extended the above demo to illustrate what I think is both the problem and solution:
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)
b2.props.label = 'Click me again'
b2.connect('clicked', lambda event: asyncio.create_task(b2_cb(event)))
self.box.add(b2)
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)
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())
The core demo is unmodified -- it lets the spinner spin for a few seconds, then starts a countdown.
The first button just does a hard sleep and obviously blocks everything -- no issue there.
The second button should immediately switch the text to "Please wait", async wait one second and then go back -- but it doesn't. Trying it in the first and the second phase of the program illustrates it best: The glib loop runs until its timeout, and only take registered event sources. In the first phase, this means it takes quite long; in the second phase, it only takes action when the countdown ticks.
The third button illustrates how I think one can get out of this: Just by stopping the GLib loop. This gives control back to the Python main loop, and it'll reschedule the GLib loop as needed. When you have clicked button 2, button 3 will flush out the queued-up action.
Next, I'll have a look at where we'd need to sprinkle such main loop quits.
Hey nice! Thanks for this good work and examples. I see that there are fixes out now (#3 and #7) for issues mentioned here and wondering if those will be merged? I am going to be using this library for a new pygtk project and could help co-maintain (although I do not really understand the main guts of this code I can at least test things).
I forked and merge some PRs into https://github.com/decentral1se/asyncio-glib and was able to get a basic Gtk example working which uses 1. the await myfuture run forever trick and 2. the call_soon_threadsafe from the glib callback trick. It seems to work! I don't really get what is going on and will probably run into a new brick wall fairly soon but that is a start at least.... we're hacking over at https://git.vvvvvvaria.org/rra/dropship/src/branch/master/dropship.py
EDIT: using https://github.com/decentral1se/trio-gtk in the end
Note that one should probably add a big warning that gtk_main_run will block the python mainloop from working.
i.e. using gtk_dialog_run and similar, while a bad idea anyway, is even worse than it is usually.