This is basically gevent, but using the GNOME platform libraries.
GLib based API's often have asynchronous versions. These tend to use the Gio pattern which looks something like this:
from gi.repository import GLib, Gio def _loaded(f, result, user_data): success, contents, etag = f.load_contents_finish(result) print len(contents) def _load_stuff(...): f = Gio.File.new_for_uri(uri) f.load_contents_async(None, _loaded, None) GLib.idle_add(_load_stuff) loop = GLib.MainLoop() loop.run()
But with a sprinkling of greenlet it looks like this:
from cringe import patch_all() patch_all() from gi.repository import GLib, Gio def _load_stuff(...): f = Gio.File.new_for_uri(uri) contents = f.load_contents() print len(contents) GLib.idle_add(_load_stuff) GLib.main()
This is still asynchronous! The cringe patch modifies the gi generated bindings to pause the currently running greenlet when it is waiting for an async operation to complete. Control can then return to the mainloop, which can process other incoming events.
All Gio-style async API's have an _async
and _finish
method. The
_async
function takes a callback
and user_data
parameter, and the
callback gets passed a AsyncResult
which can be used to collect the actual
return value.
Wrapping this in Greenlet magic gives us:
def function(*args, **kwargs): current = greenlet.getcurrent() # Call the function and pass it current.switch as the callback - this # is what allows the current coroutine to be resumed args = args + (current.switch, None) some_api_async(*args, **kwargs) # Pause the current coroutine. It will be resumed here when the # callback calls current.switch() obj, result, _ = current.parent.switch() # Actually return the expected value return some_api_finish(result)
The GLib API mostly followed the same pattern for all of its asynchronous code.
This means we can easily apply this pattern to all bindings automagically by
hooking in to GObjectMeta (in gi.types
).
We monkey-patch in our GreenletMeta which uses heuristics to patch the async API's it can recognise.