Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restart loop on demand #7

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

chrysn
Copy link

@chrysn chrysn commented Jun 26, 2020

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

This ensures that when a new task is started, the Python main loop has a
chance to run it until its first await point before the glib main loop
is resumed.
@chrysn
Copy link
Author

chrysn commented Jun 26, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant