Skip to main content

Async for a sync world

Project description

koil

codecov PyPI version Maintainer PyPI pyversions PyPI status PyPI download day

Inspiration

koil is an abstraction layer for threaded asyncio to enable "sensible defaults" for programmers working with frameworks that are barely compatible with asyncio (originally developped to get around pyqt5)

Installation

pip install koil

Main Concept

Async libraries are amazing, and its an ecosystem rapidly increasing, however in some contexts it still doesn't seem like the way to go and the burden of learning these concepts might be to high. However you developed a wonderful async api that you want to share with the world. Of couse because you care about cleaning up your resources you made it a context.

class AmazingAsyncAPI:
    def __init__(self) -> None:
        pass

    async def asleep(self):
        await asyncio.sleep(0.01)
        return "the-glory-of-async"

    async def ayielding_sleeper(self):
        for i in range(0, 20):
            await asyncio.sleep(0.01)
            yield i

    async def __aenter__(self):
        # amazing connection logic
        return self

    async def __aexit__(self, *args, **kwargs):
        # amazing tear down logic
        return self

However if somebody wants to use this api in sync environment they are in for a good one, as a call to asyncio.run() just won't do the trick. And you will have to write a lot of boilerplate code to make it work. And once you start trying to use the yielding_sleeper you will be in for a good one.

async def the_annoying_wrapper():

    async with AmazingAsyncAPI() as e:
        print(await e.sleep()) # easy enough

        # How do I use the output of the yielding
        # sleeper? Queues? Another thread?


asyncio.run(the_annoying_wrapper())

Well koil is here to help. Just mark your class with koilable and the functions that you want to be able to call from a sync context with unkoilable.

from koil import koilable, unkoil, unkoil_gen

@koilable
class AmazingAsyncAPI:
    def __init__(self) -> None:
        pass

    async def asleep(self):
        await asyncio.sleep(0.01)
        return "the-glory-of-async"

    async def ayielding_sleeper(self, limit=20):
        for i in range(0, limit):
            await asyncio.sleep(0.01)
            yield i

    
    def sleep(self):
        return unkoilable(self.asleep)

    def yielding_sleeper(self):
        return unkoil_gen(self.ayielding_sleeper, limit=20),


    async def __aenter__(self):
        # amazing connection logic
        return self

    async def __aexit__(self, *args, **kwargs):
        # amazing tear down logic
        return self

And now it works. Just use your Api with a normal context manager.

with AmazingAsyncAPI as e:
  print(e.sleep())

  for i in e.yielding_sleeper():
    print(i)

# Context manager will take care of the cleanup

How does it work?

Koil under the hood spawns a new event loop in another thread, calls functions that are marked with unkoilable threadsafe in that loop and returns the result, when exiting it shuts down the loop in the other thread.

Other usages

If you have multiple context managers or tasks that you would just like to run in another thread, we do not spawn a new thread for each of them, but rather use the same thread for all of them. This is to avoid the overhead of spawning a new thread for each context manager. On the asyncio side, all tasks will be in the same loop, so you can use asyncio primitives to communicate between them.

You can also just use Koil as a threaded event loop, and use the unkoil function to run functions in that loop.

async def task(arg):
       x = await ...
       return x

with Koil(): # creates a threaded loop

    x = unkoil(task, 1)

    with AmazingAsyncAPI as e:
       print(e.sleep())

Importantly, Koil also provides primites to run sync functions (in another thread, and experimentally in another process) and await them in the koil loop. This is useful if you have a sync function that you want to use in an async context.

from koil.helpers import run_spawned, iterate_spawned
import time

def sync_function(arg):
    return arg

def sync_generator(arg):
    for i in range(0, arg):
        time.sleep(1)
        yield i

async def run_async():
    x = await run_spawned(sync_function, 1)

    async for i in iterate_spawned(sync_generator, 1):
        print(i)

    return x

with Koil():
    x = unkoil(run_async)

THis allows you to use async primitives to communicate with sync functions. Note that this is not a good idea if you have a lot of sync functions, as the overhead of spawning a new thread for each of them is quite high. You can however pass a threadpool to the run_spawned function to avoid this overhead.

Task Support

Sometimes you want to run a task in the background and just get the result when you need it. Can't Koil do that? Well it could, but we belive this is a bad idea. You should have that in the async world. However you can browser our code and use our deprecated functions to do that. We just don't think its a good idea.

PyQt Support

One of the main reasons for koil was to get around the fact that PyQt5 is not asyncio compatible, and all the### Installation

pip install koil
from koil.qt import create_qt_koil, koilqt, unkoilqt

class KoiledInterferingFutureWidget(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.koil = create_qt_koil(parent=self) 
        # important to set parent, helps with cleanup
        # once the widget is destroyed. Loop starts automatically

        self.ado_me = koilqt(self.in_qt_task, autoresolve=True)
        # We can make qt functions callable from the async loop

        self.loop_runner = unkoilqt(self.ado_stuff_in_loop)
        self.loop_runner.returned.connect(self.task_finished)

        self.call_task_button = QtWidgets.QPushButton("Call Task")
        self.greet_label = QtWidgets.QLabel("")

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(self.call_task_button)
        layout.addWidget(self.greet_label)

        self.setLayout(layout)

        self.call_task_button.clicked.connect(self.my_coro_task.run)

    def in_qt_task(self, future: QtFuture):
        """This is a task that is run in the Qt Main Thread"""
        # We can do stuff in the Qt Thread
        # We can resolve the future at any time
        # self.on_going_future = future

        future.resolve("called")

    def task_finished(self):
        """This is called when the task is finished. Main thread"""
        self.greet_label.setText("Hello!")

    async def ado_stuff_in_loop(self):
        """This is a coroutine that is run in the threaded loop"""
        x = await self.ado_me()
        self.coroutine_was_run = True


def main():
    app = QtWidgets.QApplication(sys.argv)
    widget = KoiledInterferingFutureWidget()
    widget.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Project details


Release history Release notifications | RSS feed

This version

2.5.0

Download files

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

Source Distribution

koil-2.5.0.tar.gz (15.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

koil-2.5.0-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

File details

Details for the file koil-2.5.0.tar.gz.

File metadata

  • Download URL: koil-2.5.0.tar.gz
  • Upload date:
  • Size: 15.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for koil-2.5.0.tar.gz
Algorithm Hash digest
SHA256 7c333cbf13068096064547233055c49b3f947cd1932426aef526599ae6824e82
MD5 3ec4e81959ee0260e2181354830fb0c9
BLAKE2b-256 c39150e3969afc36caa25df41619f23a5da79c642b219be182420b5afad4ad2e

See more details on using hashes here.

File details

Details for the file koil-2.5.0-py3-none-any.whl.

File metadata

  • Download URL: koil-2.5.0-py3-none-any.whl
  • Upload date:
  • Size: 19.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for koil-2.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3f91b06f4177d9a8c8bac7a0a42ff254d5dc7226bcfd199cd34213981f87a032
MD5 e8fe6ce2efedc661ca8cf07d9ca16d32
BLAKE2b-256 bf44646d7e4c49bb4040ec451e1000857d1bb49761524d3fa77a95cd9e3a965c

See more details on using hashes here.

Supported by

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