Package: koil
koilInspirationkoil 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)InstallationpipinstallkoilMain ConceptAsync 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.classAmazingAsyncAPI:def__init__(self)->None:passasyncdefsleep(self):awaitasyncio.sleep(0.01)return"the-glory-of-async"asyncdefyielding_sleeper(self):foriinrange(0,20):awaitasyncio.sleep(0.01)yieldiasyncdef__aenter__(self):# amazing connection logicreturnselfasyncdef__aexit__(self,*args,**kwargs):# amazing tear down logicreturnselfHowever 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.asyncdefthe_annoying_wrapper():asyncwithAmazingAsyncAPI()ase:print(awaite.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.fromkoilimportkoilable,unkoilable@koilableclassAmazingAsyncAPI:def__init__(self)->None:pass@unkoilableasyncdefsleep(self):awaitasyncio.sleep(0.01)return"the-glory-of-async"@unkoilableasyncdefyielding_sleeper(self):foriinrange(0,20):awaitasyncio.sleep(0.01)yieldiasyncdef__aenter__(self):# amazing connection logicreturnselfasyncdef__aexit__(self,*args,**kwargs):# amazing tear down logicreturnselfAnd now it works. Just use your Api with a normal context manager.withAmazingAsyncAPIase:print(e.sleep())foriine.yielding_sleeper():print(i)# Context manager is closed and cleaned up```>## How does it work?Koilunderthehoodspawnsaneweventloopinanotherthread,callsfunctionsthataremarkedwithunkoilablethreadsafeinthatloopandreturnstheresult,whenexitingitshutsdowntheloopintheotherthread.### Other usagesIfyouhavemultiplecontextmanagersortasksthatyouwouldjustliketoruninanotherthread,wedo*not*spawnanewthreadforeachofthem,butratherusethesamethreadforallofthem.Thisistoavoidtheoverheadofspawninganewthreadforeachcontextmanager.Ontheasyncioside,alltaskswillbeinthesameloop,soyoucanuseasyncioprimitivestocommunicatebetweenthem.YoucanalsojustuseKoilasathreadedeventloop,andusetheunkoilfunctiontorunfunctionsinthatloop.```pythonasyncdeftask(arg):x=await...returnxwithKoil():# creates a threaded loopx=unkoil(task,1)withAmazingAsyncAPIase: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.fromkoil.helpersimportrun_spawned,iterate_spawnedimporttimedefsync_function(arg):returnargdefsync_generator(arg):foriinrange(0,arg):time.sleep(1)yieldiasyncdefrun_async():x=awaitrun_spawned(sync_function,1)asyncforiiniterate_spawned(sync_generator,1):print(i)returnxwithKoil():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 SupportSometimes 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 SupportOne of the main reasons for koil was to get around the fact that PyQt5 is not asyncio compatible, and all the### Installationpipinstallkoilfromkoil.qtimportcreate_qt_koil,koilqt,unkoilqtclassKoiledInterferingFutureWidget(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 automaticallyself.ado_me=koilqt(self.in_qt_task,autoresolve=True)# We can make qt functions callable from the async loopself.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)defin_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 = futurefuture.resolve("called")deftask_finished(self):"""This is called when the task is finished. Main thread"""self.greet_label.setText("Hello!")asyncdefado_stuff_in_loop(self):"""This is a coroutine that is run in the threaded loop"""x=awaitself.ado_me()self.coroutine_was_run=Truedefmain():app=QtWidgets.QApplication(sys.argv)widget=KoiledInterferingFutureWidget()widget.show()sys.exit(app.exec_())if__name__=="__main__":main()