Coding 2020-06-29

By Max Woerner Chase

I didn't have any real plan today, but I ended up mesing with Trio and pyglet. I followed Trio's directions for running under and arbitrary other event loop, and it worked on the first try, which is a massive testament to the quality of both projects' documentation. (I mean, we've seen how things can go when I'm not working with such libraries or frameworks at all. The odds are really stacked against things "just working"; I'm way more used to having to make them work.) Anyway, I didn't have any specific thing in mind that I needed this for, so I guess I'll just back-burner this and keep it in mind as "a thing I can do".

Here's the code I wrote, including a weird hybrid "hello world" sample. This differs from the original "just works" code in that I did some minor refactorings to simplify and shorten some of the code, and I added some basic documentation of what I did and why.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
"""Adapter widget to run the Trio and pyglet event loops together.

I tried and failed to come up with a Three Little Pigs joke.
"""

import pyglet.app
import pyglet.event
import pyglet.window
import trio.lowlevel


class TrioLoop(pyglet.event.EventDispatcher):
    """Custom widget for running a Trio event loop concurrently with pyglet."""

    started = False

    def start_trio(self, trio_main):
        """Queue the Trio mainloop to run when the app starts.

        This function should be called once, before pyglet.app.run().
        """
        if self.started:
            raise RuntimeError
        self.started = True
        pyglet.app.platform_event_loop.post_event(self, "on_trio_start", trio_main)

    def _schedule_callback(self, callback):
        """Schedule the guest loop callback in pyglet's event loop."""
        pyglet.app.platform_event_loop.post_event(self, "on_trio_callback", callback)

    def _done_callback(self, outcome):
        """Schedule the done callback in pyglet's event loop."""
        pyglet.app.platform_event_loop.post_event(self, "on_trio_done", outcome)

    def on_trio_start(self, trio_main):
        """Handle the on_trio_start event. Start Trio in guest mode."""
        trio.lowlevel.start_guest_run(
            trio_main,
            run_sync_soon_threadsafe=self._schedule_callback,
            done_callback=self._done_callback,
        )

    def on_trio_callback(self, callback):
        """Handle the on_trio_callback event."""
        callback()

    def on_trio_done(self, outcome):
        """Handle the on_trio_done event. Stop the event loop.

        This function should, but does not yet, do something with the "outcome"
        parameter.
        """
        pyglet.app.event_loop.exit()


TrioLoop.register_event_type("on_trio_start")
TrioLoop.register_event_type("on_trio_callback")
TrioLoop.register_event_type("on_trio_done")


async def mainloop():
    """Run a combination of the Trio and pyglet tutorials.

    This code isn't really useful on its own, but functions as a
    proof-of-concept and manual test.
    """
    for _ in range(5):
        window = pyglet.window.Window()
        label = pyglet.text.Label(
            "Hello from Trio",
            font_name="Times New Roman",
            font_size=36,
            x=window.width // 2,
            y=window.height // 2,
            anchor_x="center",
            anchor_y="center",
        )

        @window.event
        def on_draw():
            window.clear()
            label.draw()

        await trio.sleep(1)


if __name__ == "__main__":
    trio_loop = TrioLoop()
    trio_loop.start_trio(mainloop)
    pyglet.app.run()

Honestly, I spent some time trying to work out if I somehow implemented this wrong in a way that looks right to me for simple cases. I can't see any way that could happen, given the behavior I'm seeing. The fact that five windows get created means that the callback function is definitely being called, and that can only happen if pyglet's event loop is handling events properly. In addition, the on_draw events are firing.

Anyway, I'm sure there's a lot of potential here that I'm not seeing at the moment because I've been so caught up in low-level details. Maybe I should try to design a Zach-like to try and implement in it. I'll have to think about it.

Good night.