Skip to main content

Async library for Kivy

Project description

AsyncKivy

Youtube
日本語doc

asynckivy is an async library that saves you from ugly callback-based code, like most of async libraries do. Let's say you want to do:

  1. print('A')
  2. wait for 1sec
  3. print('B')
  4. wait for a button to be pressed
  5. print('C')

in that order. Your code would look like this:

from kivy.clock import Clock

def what_you_want_to_do(button):
    print('A')

    def one_sec_later(__):
        print('B')
        button.bind(on_press=on_button_press)
    Clock.schedule_once(one_sec_later, 1)

    def on_button_press(button):
        button.unbind(on_press=on_button_press)
        print('C')

what_you_want_to_do(...)

It's not easy to understand. If you use asynckivy, the above code will become:

import asynckivy as ak

async def what_you_want_to_do(button):
    print('A')
    await ak.sleep(1)
    print('B')
    await ak.event(button, 'on_press')
    print('C')

ak.start(what_you_want_to_do(...))

Installation

It's recommended to pin the minor version, because if it changed, it means some important breaking changes occurred.

poetry add asynckivy@~0.6
pip install "asynckivy>=0.6,<0.7"

Usage

import asynckivy as ak

async def some_task(button):
    # wait for 1sec
    dt = await ak.sleep(1)
    print(f'{dt} seconds have passed')

    # wait until a button is pressed
    await ak.event(button, 'on_press')

    # wait until 'button.x' changes
    __, x = await ak.event(button, 'x')
    print(f'button.x is now {x}')

    # wait until 'button.x' becomes greater than 100
    if button.x <= 100:
        __, x = await ak.event(button, 'x', filter=lambda __, x: x>100)
        print(f'button.x is now {x}')

    # wait until EITHER a button is pressed OR 5sec passes.
    # i.e. wait at most 5 seconds for a button to be pressed
    tasks = await ak.wait_any(
        ak.event(button, 'on_press'),
        ak.sleep(5),
    )
    print("The button was pressed" if tasks[0].finished else "Timeout")

    # wait until a button is pressed AND 5sec passes.
    tasks = await ak.wait_all(
        ak.event(button, 'on_press'),
        ak.sleep(5),
    )

    # nest as you want.
    # wait for a button to be pressed AND (5sec OR 'other_async_func' to complete)
    tasks = await ak.wait_all(
        ak.event(button, 'on_press'),
        ak.wait_any(
            ak.sleep(5),
            other_async_func(),
        ),
    )
    child_tasks = tasks[1].result
    print("5sec passed" if child_tasks[0].finished else "other_async_func has completed")

ak.start(some_task(some_button))

animation

from types import SimpleNamespace
import asynckivy as ak

async def async_func(widget1, widget2):
    obj = SimpleNamespace(attr1=10, attr2=[20, 30, ], attr3={'key': 40, })

    # Animate attibutes of any object.
    await ak.animate(obj, attr1=200, attr2=[200, 100], attr3={'key': 400})

    # Interpolate between two values in an async-manner.
    async for v in ak.interpolate(0, 200):
        print(v)
        # await something  # DO NOT await anything during this loop

    # fade-out widgets, excute the with-block, fade-in widgets.
    async with ak.fade_transition(widget1, widget2):
        widget.text = 'new text'
        widget2.y = 200

    # If you want more low-level control over animations, use the vanim module.
    from asynckivy import vanim
    async for dt in vanim.delta_time():
        ...

touch handling

You can easily handle on_touch_xxx events via asynckivy.rest_of_touch_events().

class TouchReceiver(Widget):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.opos):
            ak.start(self.handle_touch(touch))
            return True

    async def handle_touch(self, touch):
        print('on_touch_up')
        async for __ in ak.rest_of_touch_events(self, touch):
            # await something  # DO NOT await anything during this loop
            print('on_touch_move')
        print('on_touch_up')

If Kivy is running in asyncio/trio mode, rest_of_touch_events() might not work. In that case, use watch_touch().

import asynckivy as ak

class TouchReceiver(Widget):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.opos):
            ak.start(self.handle_touch(touch))
            return True
        return super().on_touch_down(touch)

    async def handle_touch(self, touch):
        print('on_touch_up')
        async with ak.watch_watch(self, touch) as in_progress:
            # DO NOT await anything inside this with-block except the return value of 'in_progress()'.
            while await in_progress():
                print('on_touch_move')
        print('on_touch_up')

Notes

Places you cannot await

I already mentioned about this but I'll say again. You cannot await while iterating rest_of_touch_events() or interpolate().

import asynckivy as ak

async def async_fn():
    async for v in ak.interpolate(...):
        await something  # <-- NOT ALLOWED

    async for __ in ak.rest_of_touch_events(...):
        await something  # <-- NOT ALLOWED

Some of features might not work if Kivy is running in asyncio/trio mode

asyncio and trio do some hacky stuff, sys.set_asyncgen_hooks() and sys.get_asyncgen_hooks, which likely hinders asynckivy-flavored async generators. You can see its details here.

Because of that, the features that create async generators might not work perfectly. Here is a list of them:

  • rest_of_touch_events()
  • the entire vanim module
  • fade_transition()

I don't know how to make it work. Maybe if PEP355 is accepted, it might work.

No global state

asynckivy and its foundation, asyncgui, don't have any type of global states.

Tested on

  • CPython 3.8 + Kivy 2.2.1
  • CPython 3.9 + Kivy 2.2.1
  • CPython 3.10 + Kivy 2.2.1
  • CPython 3.11 + Kivy 2.2.1

Why this even exists

Kivy supports two legit async libraries, asyncio and Trio, from version 2.0.0 so developing another one seems reinventing the wheel. Actually, I started this one just for learning how async/await works so it was initially "reinventing the wheel".

But after playing with Trio and Kivy for a while, I noticed that Trio is not suitable for the situation where fast reactions are required e.g. touch events. The same is true of asyncio. You can confirm it by running investigation/why_xxx_is_not_suitable_for_handling_touch_events.py, and mashing a mouse button. You'll see sometimes up is not paired with down. You'll see the coordinates aren't relative to the RelativeLayout even though the target belongs to it.

The cause of those problems is that trio.Event.set() and asyncio.Event.set() don't immediately resume the tasks waiting for the Event to be set. They just schedule the tasks to resume. Same thing can be said to nursery.start_soon() and asyncio.create_task().

Trio and asyncio are async I/O libraries after all. They probably don't have to immediately resumes/starts tasks, which I think necessary for Kivy's touch handling. (If a touch is not handled immediately, its state may change). Their core design might not be suitable for GUI in the first place. That's why I'm still developing this asynckivy library to this day.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

asynckivy-0.6.0.tar.gz (16.7 kB view hashes)

Uploaded Source

Built Distribution

asynckivy-0.6.0-py3-none-any.whl (18.1 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page