diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2269b3a..e17fd1f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -10,13 +10,11 @@ on: branches: [ main ] jobs: - pygame: + noname: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11'] - env: - SDL_VIDEODRIVER: dummy + python-version: ['3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 941d6ce..4cb1714 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Clock -An event scheduler. +*An event scheduler designed for asyncgui programs.* + +First, take a look at the callback-style code below that has nothing to do with `asyncgui`. +If you've ever used `Kivy` or `Pyglet`, you may find it familiar. ```python from asyncgui_ext.clock import Clock @@ -13,34 +16,35 @@ clock.schedule_once(lambda dt: print("Hello"), 20) # Advances the clock by 10 time units. clock.tick(10) -# The clock advanced by a total of 20 time units, and the callback function will be called. +# The clock advanced by a total of 20 time units. +# The callback function will be called. clock.tick(10) # => Hello ``` -It also supports async-style APIs. The code below does the same thing as the previous one but in an async-style. +Next one is async/await-style code that involves `asyncgui`, and does the same thing as the previous. ```python import asyncgui -from asyncgui_ext.clock import Clock +from asyncgui_ext.clock import Clock, sleep clock = Clock() -async def main(): - await clock.sleep(20) +async def async_fn(): + await sleep(clock, 20) print("Hello") -asyncgui.start(main()) +asyncgui.start(async_fn()) clock.tick(10) clock.tick(10) # => Hello ``` -The two examples above effectively illustrate how this module works, but they are not practical. +These two examples effectively illustrate how this module works but they are not practical. In a real-world program, you probably want to call ``clock.tick()`` in a loop or schedule it to be called repeatedly using another scheduling API. -For example, if you are using `PyGame`, you want to do: +For example, if you are using `PyGame`, you may want to do: ```python clock = pygame.time.Clock() -vclock = asyncui_ext.clock.Clock() +vclock = asyncgui_ext.clock.Clock() # main loop while running: @@ -50,26 +54,29 @@ while running: vclock.tick(dt) ``` -And if you are using `Kivy`, you want to do: +And if you are using `Kivy`, you may want to do: ```python from kivy.clock import Clock -vclock = asyncui_ext.clock.Clock() +vclock = asyncui_ext.clock.Clock() Clock.schedule_interval(vclock.tick, 0) ``` ## Installation +Pin the minor version. + ``` -poetry add asyncgui-ext-clock@~0.1 -pip install "asyncgui-ext-clock>=0.1,<0.2" +poetry add asyncgui-ext-clock@~0.2 +pip install "asyncgui-ext-clock>=0.2,<0.3" ``` ## Tested on - CPython 3.10 - CPython 3.11 +- CPython 3.12 ## Misc diff --git a/pyproject.toml b/pyproject.toml index ccaa8e0..0927a11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "asyncgui-ext-clock" -version = "0.1.1.dev0" +version = "0.2.0" description = "" authors = ["Nattōsai Mitō "] license = "MIT" @@ -15,6 +15,7 @@ classifiers=[ 'Programming Language :: Python', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Software Development :: Libraries', 'Operating System :: OS Independent', ] diff --git a/sphinx/conf.py b/sphinx/conf.py index 8e38050..f6038f1 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -87,23 +87,8 @@ def modify_signature(app, what: str, name: str, obj, options, signature, return_ if name in group1: print(f"Hide the signature of {name!r}") return ('', None) - if name.startswith("Transition."): - return ('(p)', None) return (signature, return_annotation, ) -def modify_docstring(app, what, name, obj, options, lines, - prefix="asyncgui_ext.clock.", len_prefix=len("asyncgui_ext.clock."), - group1={'ClockEvent', }, - ): - if not name.startswith(prefix): - return - name = name[len_prefix:] - if name.startswith("Transition."): - name = name[len("Transition."):] - lines.append(f".. image:: images/transition/{name}.png") - - def setup(app): app.connect('autodoc-process-signature', modify_signature) - app.connect('autodoc-process-docstring', modify_docstring) diff --git a/sphinx/index.rst b/sphinx/index.rst index e9d4be0..2955da8 100644 --- a/sphinx/index.rst +++ b/sphinx/index.rst @@ -1,9 +1,9 @@ -============= -API Reference -============= +========================== +Clock (AsyncGui Extension) +========================== +.. toctree:: + :hidden: -.. automodule:: asyncgui_ext.clock - :members: - :undoc-members: - :exclude-members: + readme_jp + reference diff --git a/sphinx/readme_jp.rst b/sphinx/readme_jp.rst new file mode 100644 index 0000000..45b5f94 --- /dev/null +++ b/sphinx/readme_jp.rst @@ -0,0 +1,94 @@ +=========== +ReadMe |ja| +=========== + +このモジュールは :mod:`asyncgui` を用いるプログラム向けのタイマー機能を提供します。 +機能は大別すると :class:`Clock` とその他になり、それぞれ以下の特徴を持ちます。 + +* ``Clock`` ... コールバック型のAPIで ``asyncgui`` の用いないプログラムからも利用できる。 +* その他 ... async/await型のAPIで ``asyncgui`` を用いるプログラムからのみ利用できる。 + +まずはコールバック型のAPIのみを用いた以下のコードを見てください。 + +.. code-block:: + + from asyncgui_ext.clock import Clock + + clock = Clock() + + # 20経過後に関数が呼ばれるようにする。 + clock.schedule_once(lambda dt: print("Hello"), 20) + + # 時計を10進める。 + clock.tick(10) + + # 合計で20進むので関数が呼ばれる。 + clock.tick(10) # => Hello + +:mod:`sched` と同じで時間の単位が決まってない事に気付いたと思います。 +APIに渡す時間の単位は統一さえされていれば何でも構いません。 + +次はasync/await型のAPIを用いた以下のコードを見てください。 + +.. code-block:: + + import asyncgui + from asyncgui_ext.clock import Clock, sleep + + clock = Clock() + + async def async_fn(): + await sleep(clock, 20) + print("Hello") + + asyncgui.start(async_fn()) + clock.tick(10) + clock.tick(10) # => Hello + +この様に ``clock.tick()`` を呼ぶ事で時計内部の時が進み、関数が呼ばれたり停止中のタスクが再開するわけです。 +しかしこれらの例はこのモジュールの仕組みを示しているだけであまり実用的ではありません。 +実際のプログラムでは ``clock.tick()`` をループ内で呼んだり別のタイマーを用いて定期的に呼ぶ事になると思います。 +例えば ``PyGame`` を使っているなら以下のように、 + +.. code-block:: + + clock = pygame.time.Clock() + vclock = asyncgui_ext.clock.Clock() + + # メインループ + while running: + ... + + dt = clock.tick(fps) + vclock.tick(dt) + +``Kivy`` を使っているなら以下のようになると思います。 + +.. code-block:: + + from kivy.clock import Clock + + vclock = asyncui_ext.clock.Clock() + Clock.schedule_interval(vclock.tick, 0) + +インストール方法 +----------------------- + +マイナーバージョンまでを固定してください。 + +:: + + poetry add asyncgui-ext-clock@~0.2 + pip install "asyncgui-ext-clock>=0.2,<0.3" + +テスト環境 +----------------------- + +* CPython 3.10 +* CPython 3.11 +* CPython 3.12 + +その他 +----------------------- + +* [YouTube](https://youtu.be/kPVzO8fF0yg) (Kivy上で使う例) diff --git a/sphinx/reference.rst b/sphinx/reference.rst new file mode 100644 index 0000000..e9d4be0 --- /dev/null +++ b/sphinx/reference.rst @@ -0,0 +1,9 @@ +============= +API Reference +============= + + +.. automodule:: asyncgui_ext.clock + :members: + :undoc-members: + :exclude-members: diff --git a/src/asyncgui_ext/clock.py b/src/asyncgui_ext/clock.py index 46c7378..84bef9e 100644 --- a/src/asyncgui_ext/clock.py +++ b/src/asyncgui_ext/clock.py @@ -1,10 +1,16 @@ -__all__ = ('ClockEvent', 'Clock', 'Transition', ) +__all__ = ( + 'ClockEvent', 'Clock', + 'sleep', 'move_on_after', 'n_frames', + 'anim_attrs', 'anim_attrs_abbr', + 'anim_with_dt', 'anim_with_et', 'anim_with_dt_et', 'anim_with_ratio', 'anim_with_dt_et_ratio', + 'interpolate_scalar', 'interpolate_sequence', + 'run_in_thread', 'run_in_executor', +) import types from typing import TypeAlias, TypeVar from collections.abc import Callable, Awaitable, AsyncIterator from functools import partial -import math from dataclasses import dataclass from contextlib import AbstractAsyncContextManager from threading import Thread @@ -16,187 +22,6 @@ ClockCallback: TypeAlias = Callable[[TimeUnit], None] -class Transition: - ''' - A copy of :class:`kivy.animation.AnimationTransition`. - ''' - def linear(p): - return p - - def in_quad(p): - return p * p - - def out_quad(p): - return -1.0 * p * (p - 2.0) - - def in_out_quad(p): - p = p * 2 - if p < 1: - return 0.5 * p * p - p -= 1.0 - return -0.5 * (p * (p - 2.0) - 1.0) - - def in_cubic(p): - return p * p * p - - def out_cubic(p): - p = p - 1.0 - return p * p * p + 1.0 - - def in_out_cubic(p): - p = p * 2 - if p < 1: - return 0.5 * p * p * p - p -= 2 - return 0.5 * (p * p * p + 2.0) - - def in_quart(p): - return p * p * p * p - - def out_quart(p): - p = p - 1.0 - return -1.0 * (p * p * p * p - 1.0) - - def in_out_quart(p): - p = p * 2 - if p < 1: - return 0.5 * p * p * p * p - p -= 2 - return -0.5 * (p * p * p * p - 2.0) - - def in_quint(p): - return p * p * p * p * p - - def out_quint(p): - p = p - 1.0 - return p * p * p * p * p + 1.0 - - def in_out_quint(p): - p = p * 2 - if p < 1: - return 0.5 * p * p * p * p * p - p -= 2.0 - return 0.5 * (p * p * p * p * p + 2.0) - - def in_sine(p, cos=math.cos, pi=math.pi): - return -1.0 * cos(p * (pi / 2.0)) + 1.0 - - def out_sine(p, sin=math.sin, pi=math.pi): - return sin(p * (pi / 2.0)) - - def in_out_sine(p, cos=math.cos, pi=math.pi): - return -0.5 * (cos(pi * p) - 1.0) - - def in_expo(p, pow=pow): - if p == 0: - return 0.0 - return pow(2, 10 * (p - 1.0)) - - def out_expo(p, pow=pow): - if p == 1.0: - return 1.0 - return -pow(2, -10 * p) + 1.0 - - def in_out_expo(p, pow=pow): - if p == 0: - return 0.0 - if p == 1.: - return 1.0 - p = p * 2 - if p < 1: - return 0.5 * pow(2, 10 * (p - 1.0)) - p -= 1.0 - return 0.5 * (-pow(2, -10 * p) + 2.0) - - def in_circ(p, sqrt=math.sqrt): - return -1.0 * (sqrt(1.0 - p * p) - 1.0) - - def out_circ(p, sqrt=math.sqrt): - p = p - 1.0 - return sqrt(1.0 - p * p) - - def in_out_circ(p, sqrt=math.sqrt): - p = p * 2 - if p < 1: - return -0.5 * (sqrt(1.0 - p * p) - 1.0) - p -= 2.0 - return 0.5 * (sqrt(1.0 - p * p) + 1.0) - - def in_elastic(p, sin=math.sin, pi=math.pi, pow=pow): - p = .3 - s = p / 4.0 - q = p - if q == 1: - return 1.0 - q -= 1.0 - return -(pow(2, 10 * q) * sin((q - s) * (2 * pi) / p)) - - def out_elastic(p, sin=math.sin, pi=math.pi, pow=pow): - p = .3 - s = p / 4.0 - q = p - if q == 1: - return 1.0 - return pow(2, -10 * q) * sin((q - s) * (2 * pi) / p) + 1.0 - - def in_out_elastic(p, sin=math.sin, pi=math.pi, pow=pow): - p = .3 * 1.5 - s = p / 4.0 - q = p * 2 - if q == 2: - return 1.0 - if q < 1: - q -= 1.0 - return -.5 * (pow(2, 10 * q) * sin((q - s) * (2.0 * pi) / p)) - else: - q -= 1.0 - return pow(2, -10 * q) * sin((q - s) * (2.0 * pi) / p) * .5 + 1.0 - - def in_back(p): - return p * p * ((1.70158 + 1.0) * p - 1.70158) - - def out_back(p): - p = p - 1.0 - return p * p * ((1.70158 + 1) * p + 1.70158) + 1.0 - - def in_out_back(p): - p = p * 2. - s = 1.70158 * 1.525 - if p < 1: - return 0.5 * (p * p * ((s + 1.0) * p - s)) - p -= 2.0 - return 0.5 * (p * p * ((s + 1.0) * p + s) + 2.0) - - def _out_bounce_internal(t, d): - p = t / d - if p < (1.0 / 2.75): - return 7.5625 * p * p - elif p < (2.0 / 2.75): - p -= (1.5 / 2.75) - return 7.5625 * p * p + .75 - elif p < (2.5 / 2.75): - p -= (2.25 / 2.75) - return 7.5625 * p * p + .9375 - else: - p -= (2.625 / 2.75) - return 7.5625 * p * p + .984375 - - def _in_bounce_internal(t, d, _out_bounce_internal=_out_bounce_internal): - return 1.0 - _out_bounce_internal(d - t, d) - - def in_bounce(p, _in_bounce_internal=_in_bounce_internal): - return _in_bounce_internal(p, 1.) - - def out_bounce(p, _out_bounce_internal=_out_bounce_internal): - return _out_bounce_internal(p, 1.) - - def in_out_bounce(p, _in_bounce_internal=_in_bounce_internal, _out_bounce_internal=_out_bounce_internal): - p = p * 2. - if p < 1.: - return _in_bounce_internal(p, 1.) * .5 - return _out_bounce_internal(p - 1., 1.) * .5 + .5 - - @dataclass(slots=True) class ClockEvent: _deadline: TimeUnit @@ -234,6 +59,7 @@ def current_time(self) -> TimeUnit: def tick(self, delta_time): ''' Advances the clock time and triggers scheduled events accordingly. + The ``delta_time`` must be 0 or greater. ''' self._cur_time += delta_time cur_time = self._cur_time @@ -300,406 +126,435 @@ def func(dt): self._events_to_be_added.append(event) return event - async def sleep(self, duration) -> Awaitable: - ''' - Waits for a specified period of time. - .. code-block:: +async def sleep(clock: Clock, duration) -> Awaitable: + ''' + Waits for a specified period of time. - await clock.sleep(10) - ''' - sig = ISignal() - event = self.schedule_once(sig.set, duration) + .. code-block:: - try: - await sig.wait() - except Cancelled: - event.cancel() - raise + await sleep(clock, 10) + ''' + sig = ISignal() + event = clock.schedule_once(sig.set, duration) - def move_on_after(self, timeout) -> AbstractAsyncContextManager[Task]: - ''' - Returns an async context manager that applies a time limit to its code block, - like :func:`trio.move_on_after` does. + try: + await sig.wait() + except Cancelled: + event.cancel() + raise - .. code-block:: - async with clock.move_on_after(10) as bg_task: - ... +def move_on_after(clock: Clock, timeout) -> AbstractAsyncContextManager[Task]: + ''' + Returns an async context manager that applies a time limit to its code block, + like :func:`trio.move_on_after` does. - if bg_task.finished: - print("The code block was interrupted due to a timeout") - else: - print("The code block exited gracefully.") - ''' - return wait_any_cm(self.sleep(timeout)) + .. code-block:: - @types.coroutine - def n_frames(self, n: int) -> Awaitable: - ''' - Waits for a specified number of times the :meth:`tick` to be called. + async with move_on_after(clock, 10) as bg_task: + ... - .. code-block:: + if bg_task.finished: + print("The code block was interrupted due to a timeout") + else: + print("The code block exited gracefully.") + ''' + return wait_any_cm(sleep(clock, timeout)) - await clock.n_frames(2) - If you want to wait for one time, :meth:`sleep` is preferable for a performance reason. +@types.coroutine +def n_frames(clock: Clock, n: int) -> Awaitable: + ''' + Waits for a specified number of times the :meth:`Clock.tick` to be called. - .. code-block:: + .. code-block:: - await clock.sleep(0) + await n_frames(clock, 2) - .. versionadded:: 0.1.1 - ''' - if n < 0: - raise ValueError(f"Waiting for {n} frames doesn't make sense.") + If you want to wait for one time, :func:`sleep` is preferable for a performance reason. + + .. code-block:: + + await sleep(clock, 0) + ''' + if n < 0: + raise ValueError(f"Waiting for {n} frames doesn't make sense.") + if not n: + return + + task = (yield _current_task)[0][0] + + def callback(dt): + nonlocal n + n -= 1 if not n: - return + task._step() + return False + + event = clock.schedule_interval(callback, 0) + + try: + yield _sleep_forever + finally: + event.cancel() - task = (yield _current_task)[0][0] + +async def anim_with_dt(clock: Clock, *, step=0) -> AsyncIterator[TimeUnit]: + ''' + An async form of :meth:`Clock.schedule_interval`. + + .. code-block:: + + async for dt in anim_with_dt(clock, step=10): + print(dt) + if some_condition: + break + + The code above is quivalent to the below. + + .. code-block:: def callback(dt): - nonlocal n - n -= 1 - if not n: - task._step() + print(dt) + if some_condition: return False - event = self.schedule_interval(callback, 0) + clock.schedule_interval(callback, 10) - try: - yield _sleep_forever - finally: - event.cancel() + **Restriction** - async def anim_with_dt(self, *, step=0) -> AsyncIterator[TimeUnit]: - ''' - An async form of :meth:`schedule_interval`. + You are not allowed to perform any kind of async operations during the loop. - .. code-block:: + .. code-block:: - async for dt in clock.anim_with_dt(step=10): - print(dt) - if some_condition: - break + async for dt in anim_with_dt(clock): + await awaitable # NOT ALLOWED + async with async_context_manager: # NOT ALLOWED + ... + async for __ in async_iterator: # NOT ALLOWED + ... - The code above is quivalent to the code below. + This is also true of other ``anim_with_xxx`` APIs. + ''' + async with _repeat_sleeping(clock, step) as sleep: + while True: + yield await sleep() - .. code-block:: - def callback(dt): - print(dt) - if some_condition: - return False +async def anim_with_et(clock: Clock, *, step=0) -> AsyncIterator[TimeUnit]: + ''' + Same as :func:`anim_with_dt` except this one generates the total elapsed time of the loop instead of the elapsed + time between frames. - clock.schedule_interval(callback, 10) + .. code-block:: - **Restriction** + timeout = ... + async for et in anim_with_et(clock): + ... + if et > timeout: + break + ''' + et = 0 + async with _repeat_sleeping(clock, step) as sleep: + while True: + et += await sleep() + yield et - You are not allowed to perform any kind of async operations during the loop. - .. code-block:: +async def anim_with_dt_et(clock: Clock, *, step=0) -> AsyncIterator[tuple[TimeUnit, TimeUnit]]: + ''' + :func:`anim_with_dt` and :func:`anim_with_et` combined. - async for dt in clock.anim_with_dt(): - await awaitable # NOT ALLOWED - async with async_context_manager: # NOT ALLOWED - ... - async for __ in async_iterator: # NOT ALLOWED - ... + .. code-block:: - This is also true for other ``anim_with_xxx`` APIs. - ''' - async with repeat_sleeping(self, step) as sleep: - while True: - yield await sleep() + async for dt, et in anim_with_dt_et(clock): + ... + ''' + et = 0 + async with _repeat_sleeping(clock, step) as sleep: + while True: + dt = await sleep() + et += dt + yield dt, et - async def anim_with_et(self, *, step=0) -> AsyncIterator[TimeUnit]: - ''' - Total elapsed time of iterations. - .. code-block:: +async def anim_with_ratio(clock: Clock, *, duration, step=0) -> AsyncIterator[float]: + ''' + Same as :func:`anim_with_et` except this one generates the total progression ratio of the loop. - timeout = ... - async for et in clock.anim_with_et(...): - ... - if et > timeout: - break - ''' - et = 0. - async with repeat_sleeping(self, step) as sleep: - while True: - et += await sleep() - yield et + .. code-block:: - async def anim_with_dt_et(self, *, step=0) -> AsyncIterator[tuple[TimeUnit, TimeUnit]]: - ''' - :meth:`anim_with_dt` and :meth:`anim_with_et` combined. + async for p in anim_with_ratio(clock, duration=...): + print(p * 100, "%") - .. code-block:: + If you want to progress at a non-consistant rate, you may find the + `source code `__ + of the :class:`kivy.animation.AnimationTransition` helpful. - async for dt, et in clock.anim_with_dt_et(...): - ... - ''' - et = 0. - async with repeat_sleeping(self, step) as sleep: - while True: - dt = await sleep() - et += dt - yield dt, et + .. code-block:: - async def anim_with_ratio(self, *, duration, step=0) -> AsyncIterator[float]: - ''' - .. code-block:: + async for p in anim_with_ratio(clock, duration=...): + p = p * p # quadratic + print(p * 100, "%") + ''' + if not duration: + await sleep(clock, step) + yield 1.0 + return + et = 0 + async with _repeat_sleeping(clock, step) as sleep_: + while et < duration: + et += await sleep_() + yield et / duration + + +async def anim_with_dt_et_ratio(clock: Clock, *, duration, step=0) -> AsyncIterator[tuple[TimeUnit, TimeUnit, float]]: + ''' + :func:`anim_with_dt`, :func:`anim_with_et` and :func:`anim_with_ratio` combined. - async for p in clock.anim_with_ratio(duration=...): - print(p * 100, "%") - ''' + .. code-block:: + + async for dt, et, p in anim_with_dt_et_ratio(clock): + ... + ''' + async with _repeat_sleeping(clock, step) as sleep: if not duration: - await self.sleep(step) - yield 1.0 + dt = await sleep() + yield dt, dt, 1.0 return et = 0. - async with repeat_sleeping(self, step) as sleep: - while et < duration: - et += await sleep() - yield et / duration + while et < duration: + dt = await sleep() + et += dt + yield dt, et, et / duration - async def anim_with_dt_et_ratio(self, *, duration, step=0) -> AsyncIterator[tuple[TimeUnit, TimeUnit, float]]: - ''' - :meth:`anim_with_dt`, :meth:`anim_with_et` and :meth:`anim_with_ratio` combined. - .. code-block:: +def _linear(p): + return p - async for dt, et, p in clock.anim_with_dt_et_ratio(...): - ... - ''' - async with repeat_sleeping(self, step) as sleep: - if not duration: - dt = await sleep() - yield dt, dt, 1.0 - return - et = 0. - while et < duration: - dt = await sleep() - et += dt - yield dt, et, et / duration - - async def interpolate_scalar(self, start, end, *, duration, step=0, transition=Transition.linear) -> AsyncIterator: - ''' - Interpolates between the values ``start`` and ``end`` in an async-manner. - .. code-block:: +async def interpolate_scalar(clock, start, end, *, duration, step=0, transition=_linear) -> AsyncIterator: + ''' + Interpolates between the values ``start`` and ``end`` in an async-manner. - async for v in clock.interpolate(0, 100, duration=100, step=30): - print(int(v)) - - ============ ====== - elapsed time output - ============ ====== - 0 0 - 30 30 - 60 60 - 90 90 - **120** 100 - ============ ====== - ''' - slope = end - start - yield transition(0.) * slope + start - async for p in self.anim_with_ratio(step=step, duration=duration): - if p >= 1.0: - break - yield transition(p) * slope + start - yield transition(1.) * slope + start + .. code-block:: - async def interpolate_sequence(self, start, end, *, duration, step=0, transition=Transition.linear, - output_type=tuple) -> AsyncIterator: - ''' - Same as :meth:`interpolate_scalar` except this one is for sequence type. + async for v in interpolate(clock, 0, 100, duration=100, step=30): + print(int(v)) + + ============ ====== + elapsed time output + ============ ====== + 0 0 + 30 30 + 60 60 + 90 90 + **120** 100 + ============ ====== + ''' + slope = end - start + yield transition(0.) * slope + start + async for p in anim_with_ratio(clock, step=step, duration=duration): + if p >= 1.0: + break + yield transition(p) * slope + start + yield transition(1.) * slope + start - .. code-block:: - async for v in clock.interpolate_sequence([0, 50], [100, 100], duration=100, step=30): - print(v) - - ============ ========== - elapsed time output - ============ ========== - 0 (0, 50) - 30 (30, 65) - 60 (60, 80) - 90 (90, 95) - **120** (100, 100) - ============ ========== - ''' - zip_ = zip - slope = tuple(end_elem - start_elem for end_elem, start_elem in zip_(end, start)) +async def interpolate_sequence(clock, start, end, *, duration, step=0, transition=_linear, output_type=tuple) -> AsyncIterator: + ''' + Same as :func:`interpolate_scalar` except this one is for sequence type. - p = transition(0.) - yield output_type(p * slope_elem + start_elem for slope_elem, start_elem in zip_(slope, start)) + .. code-block:: - async for p in self.anim_with_ratio(step=step, duration=duration): - if p >= 1.0: - break - p = transition(p) - yield output_type(p * slope_elem + start_elem for slope_elem, start_elem in zip_(slope, start)) + async for v in interpolate_sequence(clock, [0, 50], [100, 100], duration=100, step=30): + print(v) + + ============ ========== + elapsed time output + ============ ========== + 0 (0, 50) + 30 (30, 65) + 60 (60, 80) + 90 (90, 95) + **120** (100, 100) + ============ ========== + ''' + zip_ = zip + slope = tuple(end_elem - start_elem for end_elem, start_elem in zip_(end, start)) - p = transition(1.) + p = transition(0.) + yield output_type(p * slope_elem + start_elem for slope_elem, start_elem in zip_(slope, start)) + + async for p in anim_with_ratio(clock, step=step, duration=duration): + if p >= 1.0: + break + p = transition(p) yield output_type(p * slope_elem + start_elem for slope_elem, start_elem in zip_(slope, start)) - async def run_in_thread(self, func, *, daemon=None, polling_interval) -> Awaitable: - ''' - Creates a new thread, runs a function within it, then waits for the completion of that function. + p = transition(1.) + yield output_type(p * slope_elem + start_elem for slope_elem, start_elem in zip_(slope, start)) - .. code-block:: - return_value = await clock.run_in_thread(func) - ''' - return_value = None - exception = None - done = False - - def wrapper(): - nonlocal return_value, done, exception - try: - return_value = func() - except Exception as e: - exception = e - finally: - done = True - - Thread(target=wrapper, daemon=daemon).start() - async with repeat_sleeping(self, polling_interval) as sleep: +async def run_in_thread(clock: Clock, func, *, daemon=None, polling_interval) -> Awaitable: + ''' + Creates a new thread, runs a function within it, then waits for the completion of that function. + + .. code-block:: + + return_value = await run_in_thread(clock, func, polling_interval=...) + ''' + return_value = None + exception = None + done = False + + def wrapper(): + nonlocal return_value, done, exception + try: + return_value = func() + except Exception as e: + exception = e + finally: + done = True + + Thread(target=wrapper, daemon=daemon).start() + async with _repeat_sleeping(clock, polling_interval) as sleep: + while not done: + await sleep() + if exception is not None: + raise exception + return return_value + + +async def run_in_executor(clock: Clock, executer: ThreadPoolExecutor, func, *, polling_interval) -> Awaitable: + ''' + Runs a function within a :class:`concurrent.futures.ThreadPoolExecutor`, and waits for the completion of the + function. + + .. code-block:: + + executor = ThreadPoolExecutor() + return_value = await run_in_executor(clock, executor, func, polling_interval=...) + ''' + return_value = None + exception = None + done = False + + def wrapper(): + nonlocal return_value, done, exception + try: + return_value = func() + except Exception as e: + exception = e + finally: + done = True + + future = executer.submit(wrapper) + try: + async with _repeat_sleeping(clock, polling_interval) as sleep: while not done: await sleep() - if exception is not None: - raise exception - return return_value + except Cancelled: + future.cancel() + raise + if exception is not None: + raise exception + return return_value + + +def _update(setattr, zip, min, obj, duration, transition, output_seq_type, anim_params, task, p_time, dt): + time = p_time[0] + dt + p_time[0] = time + + # calculate progression + progress = min(1., time / duration) + t = transition(progress) + + # apply progression on obj + for attr_name, org_value, slope, is_seq in anim_params: + if is_seq: + new_value = output_seq_type( + slope_elem * t + org_elem + for org_elem, slope_elem in zip(org_value, slope) + ) + setattr(obj, attr_name, new_value) + else: + setattr(obj, attr_name, slope * t + org_value) - async def run_in_executor(self, executer: ThreadPoolExecutor, func, *, polling_interval) -> Awaitable: - ''' - Runs a function within a :class:`concurrent.futures.ThreadPoolExecutor`, and waits for the completion of the - function. + # time to stop ? + if progress >= 1.: + task._step() + return False - .. code-block:: - executor = ThreadPoolExecutor() - return_value = await clock.run_in_executor(executor, func) - ''' - return_value = None - exception = None - done = False - - def wrapper(): - nonlocal return_value, done, exception - try: - return_value = func() - except Exception as e: - exception = e - finally: - done = True - - future = executer.submit(wrapper) - try: - async with repeat_sleeping(self, polling_interval) as sleep: - while not done: - await sleep() - except Cancelled: - future.cancel() - raise - if exception is not None: - raise exception - return return_value - - def _update(setattr, zip, min, obj, duration, transition, output_seq_type, anim_params, task, p_time, dt): - time = p_time[0] + dt - p_time[0] = time - - # calculate progression - progress = min(1., time / duration) - t = transition(progress) - - # apply progression on obj - for attr_name, org_value, slope, is_seq in anim_params: - if is_seq: - new_value = output_seq_type( - slope_elem * t + org_elem - for org_elem, slope_elem in zip(org_value, slope) - ) - setattr(obj, attr_name, new_value) - else: - setattr(obj, attr_name, slope * t + org_value) - - # time to stop ? - if progress >= 1.: - task._step() - return False +_update = partial(_update, setattr, zip, min) - _update = partial(_update, setattr, zip, min) - @types.coroutine - def _anim_attrs( - self, obj, duration, step, transition, output_seq_type, animated_properties, - getattr=getattr, isinstance=isinstance, tuple=tuple, str=str, partial=partial, native_seq_types=(tuple, list), - zip=zip, Transition=Transition, _update=_update, - _current_task=_current_task, _sleep_forever=_sleep_forever, /): - if isinstance(transition, str): - transition = getattr(Transition, transition) - - # get current values & calculate slopes - anim_params = tuple( +@types.coroutine +def _anim_attrs( + clock: Clock, obj, duration, step, transition, output_seq_type, animated_properties, + getattr=getattr, isinstance=isinstance, tuple=tuple, partial=partial, native_seq_types=(tuple, list), + zip=zip, _update=_update, + _current_task=_current_task, _sleep_forever=_sleep_forever, /): + + # get current values & calculate slopes + anim_params = tuple( + ( + org_value := getattr(obj, attr_name), + is_seq := isinstance(org_value, native_seq_types), ( - org_value := getattr(obj, attr_name), - is_seq := isinstance(org_value, native_seq_types), - ( - org_value := tuple(org_value), - slope := tuple(goal_elem - org_elem for goal_elem, org_elem in zip(goal_value, org_value)), - ) if is_seq else (slope := goal_value - org_value), - ) and (attr_name, org_value, slope, is_seq, ) - for attr_name, goal_value in animated_properties.items() + org_value := tuple(org_value), + slope := tuple(goal_elem - org_elem for goal_elem, org_elem in zip(goal_value, org_value)), + ) if is_seq else (slope := goal_value - org_value), + ) and (attr_name, org_value, slope, is_seq, ) + for attr_name, goal_value in animated_properties.items() + ) + + try: + event = clock.schedule_interval( + partial(_update, obj, duration, transition, output_seq_type, anim_params, (yield _current_task)[0][0], [0, ]), + step, ) + yield _sleep_forever + finally: + event.cancel() - try: - event = self.schedule_interval( - partial(_update, obj, duration, transition, output_seq_type, anim_params, (yield _current_task)[0][0], [0, ]), - step, - ) - yield _sleep_forever - finally: - event.cancel() - del _update +del _update - def anim_attrs(self, obj, *, duration, step=0, transition=Transition.linear, output_seq_type=tuple, - **animated_properties) -> Awaitable: - ''' - Animates attibutes of any object. - .. code-block:: +def anim_attrs(clock, obj, *, duration, step=0, transition=_linear, output_seq_type=tuple, + **animated_properties) -> Awaitable: + ''' + Animates attibutes of any object. - import types + .. code-block:: - obj = types.SimpleNamespace(x=0, size=(200, 300)) - await clock.anim_attrs(obj, x=100, size=(400, 400)) + import types - The ``output_seq_type`` parameter. + obj = types.SimpleNamespace(x=0, size=(200, 300)) + await anim_attrs(clock, obj, x=100, size=(400, 400)) - .. code-block:: + The ``output_seq_type`` parameter. - obj = types.SimpleNamespace(size=(200, 300)) - await clock.anim_attrs(obj, size=(400, 400), output_seq_type=list) - assert type(obj.size) is list - ''' - return self._anim_attrs(obj, duration, step, transition, output_seq_type, animated_properties) + .. code-block:: - def anim_attrs_abbr(self, obj, *, d, s=0, t=Transition.linear, output_seq_type=tuple, - **animated_properties) -> Awaitable: - ''' - :meth:`anim_attrs` cannot animate attributes named ``step``, ``duration`` and ``transition`` but this one can. - ''' - return self._anim_attrs(obj, d, s, t, output_seq_type, animated_properties) + obj = types.SimpleNamespace(size=(200, 300)) + await anim_attrs(clock, obj, size=(400, 400), output_seq_type=list) + assert type(obj.size) is list + ''' + return _anim_attrs(clock, obj, duration, step, transition, output_seq_type, animated_properties) + + +def anim_attrs_abbr(clock, obj, *, d, s=0, t=_linear, output_seq_type=tuple, **animated_properties) -> Awaitable: + ''' + :func:`anim_attrs` cannot animate attributes named ``step``, ``duration`` and ``transition`` but this one can. + ''' + return _anim_attrs(clock, obj, d, s, t, output_seq_type, animated_properties) -class repeat_sleeping: +class _repeat_sleeping: __slots__ = ('_timer', '_interval', '_event', ) def __init__(self, clock: Clock, interval): diff --git a/tests/clock/test_anim_attrs.py b/tests/clock/test_anim_attrs.py index 79211e9..179f7e2 100644 --- a/tests/clock/test_anim_attrs.py +++ b/tests/clock/test_anim_attrs.py @@ -4,9 +4,10 @@ def test_scalar(clock): from types import SimpleNamespace import asyncgui + from asyncgui_ext.clock import anim_attrs obj = SimpleNamespace(num=0) - task = asyncgui.start(clock.anim_attrs(obj, num=20, duration=100)) + task = asyncgui.start(anim_attrs(clock, obj, num=20, duration=100)) assert int(obj.num) == 0 clock.tick(30) @@ -24,9 +25,10 @@ def test_sequence(clock): from types import SimpleNamespace from pytest import approx import asyncgui + from asyncgui_ext.clock import anim_attrs obj = SimpleNamespace(pos=[0, 100]) - task = asyncgui.start(clock.anim_attrs(obj, pos=[100, 0], duration=100)) + task = asyncgui.start(anim_attrs(clock, obj, pos=[100, 0], duration=100)) assert obj.pos == approx([0, 100]) clock.tick(30) @@ -44,9 +46,10 @@ def test_sequence(clock): def test_seq_type_parameter(clock, output_seq_type): from types import SimpleNamespace import asyncgui + from asyncgui_ext.clock import anim_attrs obj = SimpleNamespace(size=(0, 0), pos=[0, 0]) - task = asyncgui.start(clock.anim_attrs(obj, size=[10, 10], pos=(10, 10), duration=10, output_seq_type=output_seq_type)) + task = asyncgui.start(anim_attrs(clock, obj, size=[10, 10], pos=(10, 10), duration=10, output_seq_type=output_seq_type)) clock.tick(0) assert type(obj.size) is output_seq_type assert type(obj.pos) is output_seq_type diff --git a/tests/clock/test_anim_with_xxx.py b/tests/clock/test_anim_with_xxx.py index a595e9f..9a71902 100644 --- a/tests/clock/test_anim_with_xxx.py +++ b/tests/clock/test_anim_with_xxx.py @@ -1,9 +1,10 @@ def test_anim_with_dt(clock): from asyncgui import start + from asyncgui_ext.clock import anim_with_dt dt_list = [] async def async_fn(): - async for dt in clock.anim_with_dt(step=10): + async for dt in anim_with_dt(clock, step=10): dt_list.append(dt) task = start(async_fn()) @@ -23,10 +24,11 @@ async def async_fn(): def test_anim_with_et(clock): from asyncgui import start + from asyncgui_ext.clock import anim_with_et et_list = [] async def async_fn(): - async for et in clock.anim_with_et(step=10): + async for et in anim_with_et(clock, step=10): et_list.append(et) task = start(async_fn()) @@ -47,10 +49,11 @@ async def async_fn(): def test_anim_with_ratio(clock): from pytest import approx from asyncgui import start + from asyncgui_ext.clock import anim_with_ratio p_list = [] async def async_fn(): - async for p in clock.anim_with_ratio(step=10, duration=100): + async for p in anim_with_ratio(clock, step=10, duration=100): p_list.append(p) task = start(async_fn()) @@ -72,10 +75,11 @@ async def async_fn(): def test_anim_with_ratio_zero_duration(clock): from asyncgui import start + from asyncgui_ext.clock import anim_with_ratio p_list = [] async def async_fn(): - async for p in clock.anim_with_ratio(step=10, duration=0): + async for p in anim_with_ratio(clock, step=10, duration=0): p_list.append(p) task = start(async_fn()) @@ -89,10 +93,11 @@ async def async_fn(): def test_anim_with_dt_et(clock): from asyncgui import start + from asyncgui_ext.clock import anim_with_dt_et values = [] async def async_fn(): - async for v in clock.anim_with_dt_et(step=10): + async for v in anim_with_dt_et(clock, step=10): values.extend(v) task = start(async_fn()) @@ -114,10 +119,11 @@ async def async_fn(): def test_anim_with_dt_et_ratio(clock): from pytest import approx from asyncgui import start + from asyncgui_ext.clock import anim_with_dt_et_ratio values = [] async def async_fn(): - async for v in clock.anim_with_dt_et_ratio(step=10, duration=100): + async for v in anim_with_dt_et_ratio(clock, step=10, duration=100): values.extend(v) task = start(async_fn()) @@ -148,10 +154,11 @@ async def async_fn(): def test_anim_with_dt_et_ratio_zero_duration(clock): from asyncgui import start + from asyncgui_ext.clock import anim_with_dt_et_ratio values = [] async def async_fn(): - async for v in clock.anim_with_dt_et_ratio(step=10, duration=0): + async for v in anim_with_dt_et_ratio(clock, step=10, duration=0): values.extend(v) task = start(async_fn()) diff --git a/tests/clock/test_schedule_xxx.py b/tests/clock/test_clock.py similarity index 100% rename from tests/clock/test_schedule_xxx.py rename to tests/clock/test_clock.py diff --git a/tests/clock/test_etc.py b/tests/clock/test_etc.py index f55b7c9..5eca384 100644 --- a/tests/clock/test_etc.py +++ b/tests/clock/test_etc.py @@ -2,14 +2,15 @@ def test_sleep(clock): from asyncgui import start + from asyncgui_ext.clock import sleep task_state = None async def async_fn(): nonlocal task_state task_state = 'A' - await clock.sleep(10) + await sleep(clock, 10) task_state = 'B' - await clock.sleep(10) + await sleep(clock, 10) task_state = 'C' task = start(async_fn()) @@ -23,15 +24,16 @@ async def async_fn(): def test_move_on_after(clock): from asyncgui import start + from asyncgui_ext.clock import move_on_after, sleep task_state = None async def async_fn(): - async with clock.move_on_after(15) as bg_task: + async with move_on_after(clock, 15) as bg_task: nonlocal task_state task_state = 'A' - await clock.sleep(10) + await sleep(clock, 10) task_state = 'B' - await clock.sleep(10) + await sleep(clock, 10) task_state = 'C' assert bg_task.finished @@ -52,8 +54,9 @@ def test_weakref(clock): @pytest.mark.parametrize("n", range(3)) def test_n_frames(clock, n): from asyncgui import start + from asyncgui_ext.clock import n_frames - task = start(clock.n_frames(n)) + task = start(n_frames(clock, n)) for __ in range(n): assert not task.finished clock.tick(0) diff --git a/tests/clock/test_interpolate_xxx.py b/tests/clock/test_interpolate_xxx.py index b8feecb..a18eac4 100644 --- a/tests/clock/test_interpolate_xxx.py +++ b/tests/clock/test_interpolate_xxx.py @@ -3,10 +3,11 @@ def test_interpolate_scalar(clock): from asyncgui import start + from asyncgui_ext.clock import interpolate_scalar values = [] async def async_fn(): - async for v in clock.interpolate_scalar(100, 0, duration=100): + async for v in interpolate_scalar(clock, 100, 0, duration=100): values.append(int(v)) task = start(async_fn()) @@ -25,10 +26,11 @@ async def async_fn(): @pytest.mark.parametrize('step', [0, 10]) def test_interpolate_scalar_zero_duration(clock, step): from asyncgui import start + from asyncgui_ext.clock import interpolate_scalar values = [] async def async_fn(): - async for v in clock.interpolate_scalar(100, 0, duration=0, step=step): + async for v in interpolate_scalar(clock, 100, 0, duration=0, step=step): values.append(int(v)) task = start(async_fn()) @@ -40,10 +42,11 @@ async def async_fn(): def test_interpolate_sequence(clock): from asyncgui import start + from asyncgui_ext.clock import interpolate_sequence values = [] async def async_fn(): - async for v1, v2 in clock.interpolate_sequence([0, 100], [100, 0], duration=100): + async for v1, v2 in interpolate_sequence(clock, [0, 100], [100, 0], duration=100): values.append(int(v1)) values.append(int(v2)) @@ -63,10 +66,11 @@ async def async_fn(): @pytest.mark.parametrize('step', [0, 10]) def test_interpolate_sequence_zero_duration(clock, step): from asyncgui import start + from asyncgui_ext.clock import interpolate_sequence values = [] async def async_fn(): - async for v1, v2 in clock.interpolate_sequence([0, 100], [100, 0], duration=0, step=step): + async for v1, v2 in interpolate_sequence(clock, [0, 100], [100, 0], duration=0, step=step): values.append(int(v1)) values.append(int(v2)) diff --git a/tests/clock/test_run_in_executor.py b/tests/clock/test_run_in_executor.py index bea0b9a..9da458e 100644 --- a/tests/clock/test_run_in_executor.py +++ b/tests/clock/test_run_in_executor.py @@ -5,10 +5,11 @@ def test_thread_id(clock): from asyncgui import start + from asyncgui_ext.clock import run_in_executor async def job(): before = threading.get_ident() - await clock.run_in_executor(executor, lambda: None, polling_interval=0) + await run_in_executor(clock, executor, lambda: None, polling_interval=0) after = threading.get_ident() assert before == after @@ -20,10 +21,11 @@ async def job(): def test_propagate_exception(clock): from asyncgui import start + from asyncgui_ext.clock import run_in_executor async def job(): with pytest.raises(ZeroDivisionError): - await clock.run_in_executor(executor, lambda: 1 / 0, polling_interval=0) + await run_in_executor(clock, executor, lambda: 1 / 0, polling_interval=0) with ThreadPoolExecutor() as executor: task = start(job()) @@ -33,9 +35,10 @@ async def job(): def test_no_exception(clock): from asyncgui import start + from asyncgui_ext.clock import run_in_executor async def job(): - assert 'A' == await clock.run_in_executor(executor, lambda: 'A', polling_interval=0) + assert 'A' == await run_in_executor(clock, executor, lambda: 'A', polling_interval=0) with ThreadPoolExecutor() as executor: task = start(job()) @@ -46,11 +49,12 @@ async def job(): def test_cancel_before_getting_excuted(clock): import time from asyncgui import Event, start + from asyncgui_ext.clock import run_in_executor flag = Event() async def job(): - await clock.run_in_executor(executor, flag.set, polling_interval=0) + await run_in_executor(clock, executor, flag.set, polling_interval=0) with ThreadPoolExecutor(max_workers=1) as executor: executor.submit(time.sleep, .1) diff --git a/tests/clock/test_run_in_thread.py b/tests/clock/test_run_in_thread.py index 4496a9c..5c5a015 100644 --- a/tests/clock/test_run_in_thread.py +++ b/tests/clock/test_run_in_thread.py @@ -6,10 +6,11 @@ @pytest.mark.parametrize('daemon', (True, False)) def test_thread_id(clock, daemon): from asyncgui import start + from asyncgui_ext.clock import run_in_thread async def job(): before = threading.get_ident() - await clock.run_in_thread(lambda: None, daemon=daemon, polling_interval=0) + await run_in_thread(clock, lambda: None, daemon=daemon, polling_interval=0) after = threading.get_ident() assert before == after @@ -22,10 +23,11 @@ async def job(): @pytest.mark.parametrize('daemon', (True, False)) def test_propagate_exception(clock, daemon): from asyncgui import start + from asyncgui_ext.clock import run_in_thread async def job(): with pytest.raises(ZeroDivisionError): - await clock.run_in_thread(lambda: 1 / 0, daemon=daemon, polling_interval=0) + await run_in_thread(clock, lambda: 1 / 0, daemon=daemon, polling_interval=0) task = start(job()) time.sleep(.01) @@ -36,9 +38,10 @@ async def job(): @pytest.mark.parametrize('daemon', (True, False)) def test_no_exception(clock, daemon): from asyncgui import start + from asyncgui_ext.clock import run_in_thread async def job(): - assert 'A' == await clock.run_in_thread(lambda: 'A', daemon=daemon, polling_interval=0) + assert 'A' == await run_in_thread(clock, lambda: 'A', daemon=daemon, polling_interval=0) task = start(job()) time.sleep(.01)