asyncio-glib icon indicating copy to clipboard operation
asyncio-glib copied to clipboard

GTK example

Open Dreamsorcerer opened this issue 5 years ago • 8 comments

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()

Dreamsorcerer avatar Jan 04 '20 16:01 Dreamsorcerer

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)

Dreamsorcerer avatar Jan 04 '20 16:01 Dreamsorcerer

There are two main pain points for getting an example like this working with asyncio-glib as it currently stands:

  1. 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.

  2. 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_future rely 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))

jhenstridge avatar Jan 07 '20 09:01 jhenstridge

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).

craftyguy avatar Feb 01 '20 18:02 craftyguy

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()).

chrysn avatar Jun 26 '20 14:06 chrysn

  1. 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.

chrysn avatar Jun 26 '20 15:06 chrysn

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).

decentral1se avatar Jul 22 '20 15:07 decentral1se

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

decentral1se avatar Jul 22 '20 17:07 decentral1se

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.

benzea avatar Nov 08 '20 17:11 benzea