Calling into Kivy

Executing sync functions in Kivy from Trio

Kivy is a GUI framework that runs an event loop that calls functions synchronously upon interactions with the GUI. Some applications also run a Trio eventloop executing async code in the Kivy thread (when kivy is run asynchronously) or in a separate thread. This package enables asynchronously executing synchronous code in the Kivy thread from Trio such that Trio is not blocked waiting for the Kivy clock to execute the code.

E.g. after the trio and kivy kivy_trio.context is initialized, the following function that updates a Kivy button’s state:

@async_run_in_kivy
def set_button(kivy_app, state):
    kivy_app.warn_button.state = state

can be scheduled to run in Kivy from within a Trio context, even when Trio is running in a differernt thread - simply by calling it:

app = App.get_running_app()
await set_button(app, 'down')

This uses the async_run_in_kivy() to automatically schedule the synchronous function with Kivy’s clock to run in the Kivy context by wrapping it in an asynchronous decorator and waiting until it’s done.

async_run_in_kivy() returns the function’s return value and catches and re-raises any exceptions in the waiting Trio context as shown in this example that sets a label to a number, raising an exception if it’s a negative value:

import trio
from kivy.app import App
from kivy.uix.label import Label
from random import random
from kivy_trio.to_kivy import async_run_in_kivy
from kivy_trio.context import initialize_shared_thread

class DemoApp(App):

    start_event = None

    def build(self):
        return Label(text='Empty')

    @async_run_in_kivy
    def update_text(self):
        val = random() - .5
        if val < 0:
            raise ValueError(f'Cannot set it to "{val}"')

        self.root.text = str(val)
        return val

    def on_start(self):
        # notify the waiting async trio that kivy started so it can proceed
        initialize_shared_thread()
        self.start_event.set()

async def run_app():
    app = DemoApp()
    app.start_event = trio.Event()

    async with trio.open_nursery() as nursery:
        # start app and wait until the app is started
        nursery.start_soon(app.async_run, 'trio')
        await app.start_event.wait()

        # now that the app is started, change the text, wait, and exit
        for _ in range(5):
            try:
                print('set label value to', await app.update_text())
            except ValueError as e:
                print(f'got exception "{e}"')
            await trio.sleep(2)

        app.stop()

trio.run(run_app)

When run, this printed e.g.:

got exception "Cannot set it to "-0.41975669370612656""
set label value to 0.2312564066758095
set label value to 0.34180029860423355
set label value to 0.054374588655983325
set label value to 0.08700667397013406

async_run_in_kivy() does the following when called as e.g. await app.update_text() in three phases:

  1. First it wraps and schedules the underlying update_text method to be called by Kivy in the Kivy thread (if they share a thread and properly initialized it just executes it directly skipping the remaining steps).

  2. Next, it waits for Kivy to execute the method, either saving its return value or catching the exception.

  3. Finally, when the function has finished or raised an exception, the waiting async line is woken up returning the return value or re-raising the exception.

Consequently, the marked synchronous function is executed in Kivy, but the return value is then passed back or any exception the function has raised is similarly re-raised in the Trio context when the line resumes.

Lifecycle and Cancellation

A kivy_run_in_async() and kivy_run_in_async_quiet() decorated function or method may only be called while the Kivy event loop and trio event loop are running. Otherwise, an exception may be raised when the function is called.

If the kivy event loop ends while the coroutine is executing in trio, such as when the Kivy GUI exits, the event will be canceled and a KivyEventCancelled exception will be injected into the generator. The coroutine will still finish executing in trio, but the result will be discarded when it’s done.

A waiting event may be explicitly canceled with KivyCallbackEvent.cancel(). As above a KivyEventCancelled exception will be injected into the generator and the coroutine will still finish executing in trio, but its result will be discarded.

E.g. given the following functions:

async def send_device_message(delay, device, message):
    await trio.sleep(delay)
    result = await device.send(message)
    return result

@kivy_run_in_async
def kivy_send_message(delay, device, message):
    try:
        response = yield mark(
            send_device_message, delay, device, message=message)
        print(f'Device responded with {response}')
    except KivyEventCancelled:
        print('Event canceled')

then if we do:

>>> dev = MyDevice()
>>> event = kivy_send_message(3, dev, 'hello')
>>> # a little later in kivy
>>> event.cancel()

this will print Event canceled.

Threading

A kivy_run_in_async() and kivy_run_in_async_quiet() decorated function is only safe to be called from the kivy thread, and generally only if the kivy_trio.context was properly initialized (if kivy and trio share the thread initialization is not stricly nessecary). The coroutine will be executed in the trio context that it was initialized to, which can be the same or another thread.

See the kivy_trio.context for details.

class kivy_trio.to_kivy.AsyncKivyBind(obj, name, current=True, **kwargs)

Bases: AsyncKivyEventQueue

Asynchronously observe kivy properties and events using this queue.

It creates an async iterator which for every iteration waits and then yields the property or event value, every time the property changes or the event is dispatched.

The yielded value is identical to the list of values passed to a function bound to the event or property with bind. So at minimum it’s a one element (for events) or two element (for properties, instance and value) list.

The interface is the same as AsyncKivyEventQueue and it supports its filter, ‘convert functions and its max_len argument. Its add_item() is automatically called by the internal binding.

Parameters
obj: EventDispatcher

See obj.

name: str

See name.

current: bool

See current.

E.g. try resizing the window and then pressing the button in this app:

from kivy_trio.to_kivy import AsyncKivyBind
from kivy.app import App
from kivy.lang import Builder
import trio

class MyApp(App):

    queue: AsyncKivyBind = None

    async def run_app(self):
        # run app and trio queue
        async with trio.open_nursery() as nursery:
            nursery.start_soon(self.run_queue)
            nursery.start_soon(self.async_run, 'trio')

    async def run_queue(self):
        # hack to ensure the app is running before binding
        await trio.sleep(1)
        async with AsyncKivyBind(obj=self.root, name='size') as queue:
            # save queue so we can add stuff from button
            self.queue = queue
            # queue will finish when stop is called below
            async for obj, value in queue:
                print(f'got {value}')

    def stop(self, *largs):
        super().stop(*largs)
        self.queue.stop()

    def build(self):
        return Builder.load_string(
            "Button:

” text: ‘Resize me’

” on_release: app.stop()”)

trio.run(MyApp().run_app)

current: bool = True

Whether the iterator should yield the current property value on its first iteration (True) or wait for the first dispatch before yielding the value (False). Defaults to True.

Note

This only works for properties and ignored form events.

name: str = ''

The property or event name to observe.

obj: Optional[EventDispatcher] = None

The EventDispatcher instance that contains the property or event being observed.

class kivy_trio.to_kivy.AsyncKivyEventQueue(filter=None, convert=None, max_len=None, **kwargs)

Bases: object

A class for asynchronously iterating values in a queue and waiting for the queue to be updated with new values from a synchronous method add_item().

An instance is an async iterator which for every iteration asynchronously waits for add_item() to add values to the queue and then yields it.

Parameters
filter: callable or None

See filter.

convert: callable or None

See convert.

max_len: int or None

If None, the queue may grow to an arbitrary length. Otherwise, it is bounded to maxlen. Once it’s full, when new items are added a corresponding number of oldest items are discarded.

Note

stop() is called automatically if kivy’s event loop exits while the queue is in its with block.

E.g. try pressing the button in this app:

from kivy_trio.to_kivy import AsyncKivyEventQueue
from kivy.app import App
from kivy.lang import Builder
import trio

class MyApp(App):

    i = 0
    queue: AsyncKivyEventQueue = None

    async def run_app(self):
        # run app and trio queue
        async with trio.open_nursery() as nursery:
            nursery.start_soon(self.run_queue)
            nursery.start_soon(self.async_run, 'trio')

    async def run_queue(self):
        async with AsyncKivyEventQueue() as queue:
            # save queue so we can add stuff from button
            self.queue = queue
            # queue will finish when stop is called below
            async for a, b in queue:
                print(f'got {a}, {b}')

    def button_pressed(self):
        # add items to queue and stop queue/app after 5
        self.queue.add_item(self.i, self.i ** 2)
        self.i += 1
        if self.i == 5:
            self.queue.stop()
            self.stop()

    def build(self):
        return Builder.load_string(
            "Button:\n"
            "    text: 'Press me'\n"
            "    on_release: app.button_pressed()")

trio.run(MyApp().run_app)
add_item(*args)

Adds the args to the queue to be returned by the async iterator.

This method may be executed from another thread that has been initialized with the trio context as described in kivy_trio.context.

Warning

If the trio side has not entered the with block, add_item() returns silently.

Note

If filter or convert was provided, these functions are called from within add_item() before enqueuing.

convert: Optional[Callable] = None

A callable that is internally called with add_item() ‘s positional arguments for each add_item() call.

When provided, the return value of convert is enqueued and returned by the iterator rather than the original value. It is helpful for callback values that need to be processed immediately in the synchronous context that adds it.

filter: Optional[Callable[[...], bool]] = None

A callable that is internally called with add_item() ‘s positional arguments for each add_item() call.

When provided, if the filter function returns false for these arguments, add_item() won’t enqueue the item.

stop(*args)

Call from the synchronous side to make the async iterator end.

This method may be executed from another thread. It is ignored though if the queue is not in the with block.

It may raise an exception if the iterator is restarted while it’s in the method or if not initialized.

May be called multiple times.

exception kivy_trio.to_kivy.EventLoopStoppedError

Bases: Exception

Exception raised in trio when it is waiting to run something in Kivy, but the Kivy app already finished or finished while waiting.

kivy_trio.to_kivy.async_run_in_kivy(func: Callable[[...], T], clock: Optional[ClockBase] = None) Callable[[...], Awaitable[T]]
kivy_trio.to_kivy.async_run_in_kivy(clock: Optional[ClockBase] = None) Callable[[Callable[[...], T]], Callable[[...], Awaitable[T]]]

Decorator that runs the given function or method in a Kivy context waiting (asynchronously) until it’s done.

It is primarily useful when kivy and trio are running in different threads and we need to run some Kivy code that change the GUI, waiting until it’s done. See kivy_trio.context and the note below for how to initialize kivy/trio so it knows the kivy/trio event loop it runs within.

Parameters
  • func – The synchronous function to be called in the Kivy event loop.

  • clock – The kivy Clock to use to schedule the function, if needed. Defaults to Clock if not provided and kivy_trio.context.kivy_clock is not set, otherwise one of them is used in that order.

E.g. the in the following app the trio async code will change a label and print the result:

import trio
from kivy.app import App
from kivy.uix.label import Label
from kivy_trio.to_kivy import async_run_in_kivy
from kivy_trio.context import initialize_shared_thread


class DemoApp(App):

    start_event = None

    def build(self):
        return Label(text='Empty')

    @async_run_in_kivy
    def update_text(self, text):
        # set the label of the text
        self.root.text = text
        return text * 2

    def on_start(self):
        # notify the waiting async trio that kivy started so it can proceed
        initialize_shared_thread()
        self.start_event.set()


async def run_app():
    app = DemoApp()
    app.start_event = trio.Event()

    async with trio.open_nursery() as nursery:
        # start app and wait until the app is started
        nursery.start_soon(app.async_run, 'trio')
        await app.start_event.wait()

        # now that the app is started, change the text, wait, and exit
        print(await app.update_text('App started'))
        await trio.sleep(2)
        print(await app.update_text('App closing'))
        await trio.sleep(2)
        app.stop()

trio.run(run_app)

When run, this prints:

App startedApp started
App closingApp closing

and exits the Kivy app.

Similarly, it will catch any exceptions in the decorated function and re-raise it in the waiting async code. E.g. If we change update_text to

@async_run_in_kivy
def update_text(self, text):
    raise ValueError(f'Cannot set it to "{text}"')

then when run we’ll get something like the following error:

Traceback (most recent call last):
   File "mod.py", line 41, in <module>
     trio.run(run_app)
   File _run.py", line 1932, in run
     raise runner.main_task_outcome.error
   File "mod_16.py", line 35, in run_app
     print(await app.update_text('App started'))
   ...
   File "mod.py", line 17, in update_text
     raise ValueError(f'Cannot set it to "{text}"')
 ValueError: Cannot set it to "App started"

Note

If Kivy is running in a different thread (or if we’re unsure which thread is running currently) it will schedule the function to be called using the Kivy Clock in the Kivy thread in the next clock frame, otherwise, if we know it’s running in the same thread (e.g. because initialize_shared_thread() was used), it may call the function directly.