diff --git a/poetry.lock b/poetry.lock index 9ca23cc..62837e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "alabaster" @@ -14,14 +14,14 @@ files = [ [[package]] name = "asyncgui" -version = "0.9.0" +version = "0.9.3" description = "A minimalistic async library that focuses on fast responsiveness" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ - {file = "asyncgui-0.9.0-py3-none-any.whl", hash = "sha256:be7736927c2722cd83c6d7dcfcba785f149f19c2c9cc089c7496eaedd2dd5850"}, - {file = "asyncgui-0.9.0.tar.gz", hash = "sha256:4d087c095a600de54fa66d3384a491de40b1aa0d5da2677905122793c2a11335"}, + {file = "asyncgui-0.9.3-py3-none-any.whl", hash = "sha256:ed73fc00c68d7e0d804eb239f2ad9c319dce57a5d31342c19aadc18f44270103"}, + {file = "asyncgui-0.9.3.tar.gz", hash = "sha256:46b283946a1814f600b502ee48082be9681097a72d8d011305e453d683a9064b"}, ] [package.dependencies] @@ -211,7 +211,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main", "dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -677,7 +677,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev", "doc"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -746,7 +746,7 @@ files = [ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] -markers = {main = "python_version < \"3.11\"", dev = "python_version < \"3.11\""} +markers = {main = "python_version == \"3.10\"", dev = "python_version == \"3.10\""} [[package]] name = "urllib3" @@ -769,4 +769,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "37993e4b9ba0e4dd0e366160c444280241c927e7ea4ae42357ff005d90e47d4d" +content-hash = "42819f3d65cf7c7db9b765d659470c5c06626d3172f781b5f5cc2ccf204da8c8" diff --git a/pyproject.toml b/pyproject.toml index f712fb7..87b714f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asyncgui-ext-clock" -version = "0.6.0" +version = "0.6.1.dev0" description = "An event scheduler for asyncgui programs" authors = ["Nattōsai Mitō "] license = "MIT" @@ -26,7 +26,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" -asyncgui = ">=0.8,<0.10" +asyncgui = ">=0.9.3,<0.10" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3" diff --git a/src/asyncgui_ext/clock.py b/src/asyncgui_ext/clock.py index 17ec13f..ad20608 100644 --- a/src/asyncgui_ext/clock.py +++ b/src/asyncgui_ext/clock.py @@ -7,9 +7,9 @@ from collections.abc import Callable, Awaitable, AsyncIterator from functools import partial from dataclasses import dataclass -from contextlib import AbstractAsyncContextManager +from contextlib import AbstractAsyncContextManager, asynccontextmanager -from asyncgui import Task, move_on_when, _sleep_forever, _current_task +from asyncgui import Task, move_on_when, _sleep_forever, _current_task, ExclusiveEvent, _wait_args_0, current_task TimeUnit = TypeVar("TimeUnit") ClockCallback: TypeAlias = Callable[[TimeUnit], None] @@ -165,6 +165,52 @@ def callback(dt): finally: event.cancel() + @asynccontextmanager + async def sleep_freq(self, step=0, *, free_to_await=False) -> AsyncIterator[Callable[[], Awaitable[TimeUnit]]]: + ''' + An async form of :meth:`schedule_interval`. The following callback-style code: + + .. code-block:: + + def callback(dt): + print(dt) + if some_condition: + event.cancel() + + event = clock.schedule_interval(callback, 10) + + is equivalent to the following async-style code: + + .. code-block:: + + async with clock.sleep_freq(10) as sleep: + while True: + dt = await sleep() + print(dt) + if some_condition: + break + + .. versionadded:: 0.6.1 + + The ``free_to_await`` parameter: + + If set to False (the default), the only permitted async operation within the with-block is ``await xxx()``, + where ``xxx`` is the identifier specified in the as-clause. To lift this restriction, set ``free_to_await`` to + True — at the cost of slightly reduced performance. + ''' + clock_event = self.schedule_interval(None, step) + try: + if free_to_await: + e = ExclusiveEvent() + clock_event.callback = e.fire + yield e.wait_args_0 + else: + task = await current_task() + clock_event.callback = task._step + yield _wait_args_0 + finally: + clock_event.cancel() + async def anim_with_dt(self, *, step=0) -> AsyncIterator[TimeUnit]: ''' An async form of :meth:`schedule_interval`. @@ -202,7 +248,7 @@ def callback(dt): This is also true of other ``anim_with_xxx`` APIs. ''' - async with _repeat_sleeping(self, step) as sleep: + async with self.sleep_freq(step) as sleep: while True: yield await sleep() @@ -223,7 +269,7 @@ async def anim_with_et(self, *, step=0) -> AsyncIterator[TimeUnit]: print(et) ''' et = 0 - async with _repeat_sleeping(self, step) as sleep: + async with self.sleep_freq(step) as sleep: while True: et += await sleep() yield et @@ -238,7 +284,7 @@ async def anim_with_dt_et(self, *, step=0) -> AsyncIterator[tuple[TimeUnit, Time ... ''' et = 0 - async with _repeat_sleeping(self, step) as sleep: + async with self.sleep_freq(step) as sleep: while True: dt = await sleep() et += dt @@ -275,7 +321,7 @@ async def anim_with_ratio(self, *, base, step=0) -> AsyncIterator[float]: The loop no longer stops when the progression reaches 1.0. ''' et = 0 - async with _repeat_sleeping(self, step) as sleep: + async with self.sleep_freq(step) as sleep: while True: et += await sleep() yield et / base @@ -294,7 +340,7 @@ async def anim_with_dt_et_ratio(self, *, base, step=0) -> AsyncIterator[tuple[Ti The ``duration`` parameter was replaced with the ``base`` parameter. The loop no longer stops when the progression reaches 1.0. ''' - async with _repeat_sleeping(self, step) as sleep: + async with self.sleep_freq(step) as sleep: et = 0. while True: dt = await sleep() @@ -466,25 +512,3 @@ def anim_attrs_abbr(self, obj, *, d, s=0, t=_linear, **animated_properties) -> A :meth:`anim_attrs` cannot animate attributes named ``step``, ``duration`` and ``transition`` but this one can. ''' return self._anim_attrs(obj, d, s, t, animated_properties) - - -class _repeat_sleeping: - __slots__ = ('_timer', '_interval', '_event', ) - - def __init__(self, clock: Clock, interval): - self._timer = clock - self._interval = interval - - @staticmethod - @types.coroutine - def _sleep(_f=_sleep_forever): - return (yield _f)[0][0] - - @types.coroutine - def __aenter__(self, _current_task=_current_task) -> Awaitable[Callable[[], Awaitable[TimeUnit]]]: - task = (yield _current_task)[0][0] - self._event = self._timer.schedule_interval(task._step, self._interval) - return self._sleep - - async def __aexit__(self, exc_type, exc_val, exc_tb): - self._event.cancel()