Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions sphinx/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,5 @@ def modify_signature(app, what: str, name: str, obj, options, signature, return_
return (signature, return_annotation, )


def modify_docstring(app, what: str, name: str, obj, options, lines, prefix="asynckivy.",
len_prefix=len("asynckivy."),
):
if not name.startswith(prefix):
return
name = name[len_prefix:]
if name == "managed_start":
from asynckivy._managed_start import __managed_start_doc__
lines.clear()
lines.extend(__managed_start_doc__.split("\n"))
return

def setup(app):
app.connect('autodoc-process-signature', modify_signature)
app.connect('autodoc-process-docstring', modify_docstring)

3 changes: 2 additions & 1 deletion src/asynckivy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
'anim_attrs_abbr',
'anim_with_ratio',
'block_touch_events',
'cancel_managed_tasks',
'event',
'event_freq',
'fade_transition',
Expand Down Expand Up @@ -36,4 +37,4 @@
from ._interpolate import interpolate, interpolate_seq, fade_transition
from ._threading import run_in_executor, run_in_thread
from ._etc import transform, sync_attr, sync_attrs, stencil_mask, stencil_widget_mask, sandwich_canvas, smooth_attr
from ._managed_start import managed_start
from ._managed_start import managed_start, cancel_managed_tasks
71 changes: 45 additions & 26 deletions src/asynckivy/_managed_start.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,57 @@
import asyncgui
from kivy.clock import Clock
import asyncgui as ag
from asyncgui import Task, start
from kivy.base import EventLoop


async def _setup():
global _global_nursery
async with asyncgui.open_nursery() as nursery:
_global_nursery = nursery
await asyncgui.sleep_forever()
_managed_tasks = []
_n_until_gc = _GC_IN_EVERY = 1000


_global_nursery = None
_root_task = asyncgui.start(_setup())
managed_start = _global_nursery.start
def _collect_garbage(STARTED=ag.TaskState.STARTED):
global _managed_tasks
_managed_tasks = [task for task in _managed_tasks if task.state is STARTED]

__managed_start_doc__ = '''
A task started with this function will be automatically cancelled when an ``App.on_stop``
event fires, if it is still running. This prevents the task from being cancelled by the garbage
collector, ensuring more reliable cleanup. You should always use this function instead of calling
``asynckivy.start`` directly, except when writing unit tests.

.. code-block::
def managed_start(aw: ag.Aw_or_Task, /) -> Task:
'''
A task started with this function will be automatically cancelled when an ``EventLoop.on_stop``
event fires, if it is still running. This prevents the task from being cancelled by the garbage
collector, ensuring more reliable cleanup. You should always use this function instead of calling
``asynckivy.start`` directly, except when writing unit tests.

task = managed_start(async_func(...))
.. code-block::

.. versionadded:: 0.7.1
'''
task = managed_start(async_func(...))

.. versionadded:: 0.7.1
.. versionchanged:: 0.10.0
Uses ``EventLoop.on_stop`` instead of ``App.on_stop``.
'''
global _n_until_gc
task = start(aw)
_managed_tasks.append(task)
_n_until_gc -= 1
if _n_until_gc <= 0:
_n_until_gc = _GC_IN_EVERY
_collect_garbage()
return task

def _schedule_teardown(dt):
from kivy.app import App
app = App.get_running_app()
if app is None:
return
app.fbind("on_stop", lambda __: _root_task.cancel())

def cancel_managed_tasks(*__):
'''
Cancels all tasks started with :func:`managed_start`.

Clock.schedule_once(_schedule_teardown)
Usually, you do not need to call this function directly, as it is automatically called when an
``EventLoop.on_stop`` event fires. However, you might need to call it manually in unit tests because
the ``EventLoop.on_stop`` event wouldn't be triggered in each test case.

.. versionadded:: 0.10.0
'''
global _managed_tasks
tasks = _managed_tasks
_managed_tasks = []
for t in tasks:
t.cancel()


EventLoop.fbind("on_stop", cancel_managed_tasks)
60 changes: 60 additions & 0 deletions tests/test_managed_start.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pytest


@pytest.fixture(scope="module", autouse=True)
def _shorten_gc_interval():
from asynckivy import _managed_start as _ms
original = _ms._GC_IN_EVERY
_ms._GC_IN_EVERY = _ms._n_until_gc = 2
yield
_ms._GC_IN_EVERY = _ms._n_until_gc = original


@pytest.fixture(autouse=True)
def _cancel_managed_tasks():
import asynckivy as ak
ak.cancel_managed_tasks()
yield
ak.cancel_managed_tasks()


async def finish_immediately():
pass


def test_finished_tasks_only():
import asynckivy as ak
from asynckivy import _managed_start as _ms

assert len(_ms._managed_tasks) == 0
assert _ms._n_until_gc == 2
ak.managed_start(finish_immediately())
assert len(_ms._managed_tasks) == 1
assert _ms._n_until_gc == 1
ak.managed_start(finish_immediately())
assert len(_ms._managed_tasks) == 0
assert _ms._n_until_gc == 2


def test_unfinished_tasks_only():
import asynckivy as ak
from asynckivy import _managed_start as _ms

assert len(_ms._managed_tasks) == 0
ak.managed_start(ak.sleep_forever())
assert len(_ms._managed_tasks) == 1
ak.managed_start(ak.sleep_forever())
assert len(_ms._managed_tasks) == 2
assert _ms._n_until_gc == _ms._GC_IN_EVERY


def test_mix_finished_tasks_and_unfinished_ones():
import asynckivy as ak
from asynckivy import _managed_start as _ms

assert len(_ms._managed_tasks) == 0
ak.managed_start(ak.sleep_forever())
assert len(_ms._managed_tasks) == 1
ak.managed_start(finish_immediately())
assert len(_ms._managed_tasks) == 1
assert _ms._n_until_gc == _ms._GC_IN_EVERY