├── .gitattribute ├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── README_jp.md ├── examples ├── README.txt ├── asking_questions.py ├── asking_questions_simplified.py ├── changing_text_with_fade_transition.py ├── loop_animation.py ├── network_io.py ├── painter2.py ├── painter3.py ├── painter4.py ├── popping_widget.py ├── progress_spinner.py ├── skippable_animation.py ├── smoothing.py ├── swipe_to_delete.py ├── swipe_to_delete_ver_rv.py ├── the_longer_you_press_the_widget_the_higher_it_hops.py ├── trio_move_on_after.py └── using_interpolate.py ├── investigation ├── compare_various_ways_to_repeat_sleeping.py ├── comparision_between_stateful_functions.py ├── github_issue_11.py ├── speed_comparision_between_various_sleep_implementations.py ├── why_asyncio_is_not_suitable_for_handling_touch_events.py ├── why_asynckivy_is_suitable_for_handling_touch_events.py └── why_trio_is_not_suitable_for_handling_touch_events.py ├── misc ├── notes.md ├── todo.md └── youtube_thumbnail.png ├── poetry.lock ├── pyproject.toml ├── roadmap.md ├── setup.cfg ├── sphinx ├── _static │ └── dummy ├── conf.py ├── index.rst ├── notes-ja.rst ├── notes.rst ├── print_asynckivy_components.py └── reference.rst ├── src └── asynckivy │ ├── __init__.py │ ├── _anim_attrs.py │ ├── _anim_with_xxx.py │ ├── _etc.py │ ├── _event.py │ ├── _exceptions.py │ ├── _interpolate.py │ ├── _managed_start.py │ ├── _sleep.py │ ├── _smooth_attr.py │ └── _threading.py └── tests ├── conftest.py ├── test_anim_attrs.py ├── test_anim_with_xxx.py ├── test_event.py ├── test_event_freq.py ├── test_fade_transition.py ├── test_interpolate.py ├── test_interpolate_seq.py ├── test_n_frames.py ├── test_rest_of_touch_events.py ├── test_run_in_executor.py ├── test_run_in_thread.py ├── test_sleep.py ├── test_suppress_event.py ├── test_sync_attr.py └── test_transform.py /.gitattribute: -------------------------------------------------------------------------------- 1 | *.apk binary 2 | 3 | *.ogg binary 4 | *.mp3 binary 5 | *.wav binary 6 | 7 | *.png binary 8 | *.jpg binary 9 | *.gif binary 10 | 11 | *.mp4 binary 12 | *.avi binary 13 | *.webm binary 14 | 15 | *.ttf binary 16 | *.ttc binary 17 | *.otf binary 18 | 19 | *.zip binary 20 | *.7z binary 21 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: "lint & unittest" 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | kivy_2_3: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 18 | env: 19 | DISPLAY: ':99.0' 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Setup env 27 | run: | 28 | sudo apt-get update 29 | sudo apt-get -y install xvfb pulseaudio xsel 30 | /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1280x720x24 -ac +extension GLX 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | python -m pip install pytest flake8 "kivy>=2.3,<2.4" "asyncgui>=0.7.2,<0.9" 35 | python -m pip install . 36 | - name: Lint with flake8 37 | run: make style 38 | - name: Test with pytest 39 | run: make test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | bin/ 3 | *.pyc 4 | *.pyo 5 | /.venv/ 6 | /asynckivy.egg-info/ 7 | /.pytest_cache/ 8 | /*kivymd*/ 9 | /dist 10 | /docs/ 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "pytest", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "pytest", 12 | "args": ["${file}"] 13 | }, 14 | { 15 | "name": "Python: Current File", 16 | "type": "python", 17 | "request": "launch", 18 | "program": "${file}", 19 | "console": "integratedTerminal" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": ".venv/bin/python" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019-2025 gottadiveintopython 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python 2 | PYTEST = $(PYTHON) -m pytest 3 | FLAKE8 = $(PYTHON) -m flake8 4 | 5 | test: 6 | env KCFG_GRAPHICS_MAXFPS=0 $(PYTEST) ./tests 7 | 8 | test_free_only_clock: 9 | env KCFG_GRAPHICS_MAXFPS=0 KCFG_KIVY_KIVY_CLOCK=free_only $(PYTEST) ./tests 10 | 11 | style: 12 | $(FLAKE8) --count --select=E9,F63,F7,F82 --show-source --statistics ./tests ./src/asynckivy ./examples 13 | $(FLAKE8) --count --max-complexity=10 --max-line-length=119 --statistics ./src/asynckivy ./examples 14 | 15 | html: 16 | sphinx-build -b html ./sphinx ./docs 17 | 18 | livehtml: 19 | sphinx-autobuild -b html ./sphinx ./docs 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AsyncKivy 2 | 3 | [Youtube](https://www.youtube.com/playlist?list=PLNdhqAjzeEGjTpmvNck4Uykps8s9LmRTJ) 4 | [日本語doc](README_jp.md) 5 | 6 | `asynckivy` is an async library that saves you from ugly callback-style code, 7 | like most of async libraries do. 8 | Let's say you want to do: 9 | 10 | 1. `print('A')` 11 | 1. wait for 1sec 12 | 1. `print('B')` 13 | 1. wait for a button to be pressed 14 | 1. `print('C')` 15 | 16 | in that order. 17 | Your code would look like this: 18 | 19 | ```python 20 | from kivy.clock import Clock 21 | 22 | def what_you_want_to_do(button): 23 | print('A') 24 | 25 | def one_sec_later(__): 26 | print('B') 27 | button.bind(on_press=on_button_press) 28 | Clock.schedule_once(one_sec_later, 1) 29 | 30 | def on_button_press(button): 31 | button.unbind(on_press=on_button_press) 32 | print('C') 33 | 34 | what_you_want_to_do(...) 35 | ``` 36 | 37 | It's not easy to understand. 38 | If you use `asynckivy`, the code above will become: 39 | 40 | ```python 41 | import asynckivy as ak 42 | 43 | async def what_you_want_to_do(button): 44 | print('A') 45 | await ak.sleep(1) 46 | print('B') 47 | await ak.event(button, 'on_press') 48 | print('C') 49 | 50 | ak.managed_start(what_you_want_to_do(...)) 51 | ``` 52 | 53 | ## Installation 54 | 55 | Pin the minor version. 56 | 57 | ```text 58 | poetry add asynckivy@~0.8 59 | pip install "asynckivy>=0.8,<0.9" 60 | ``` 61 | 62 | ## Usage 63 | 64 | ```python 65 | import asynckivy as ak 66 | 67 | async def some_task(button): 68 | # waits for 2 seconds to elapse 69 | dt = await ak.sleep(2) 70 | print(f'{dt} seconds have elapsed') 71 | 72 | # waits for a button to be pressed 73 | await ak.event(button, 'on_press') 74 | 75 | # waits for the value of 'button.x' to change 76 | __, x = await ak.event(button, 'x') 77 | print(f'button.x is now {x}') 78 | 79 | # waits for the value of 'button.x' to become greater than 100 80 | if button.x <= 100: 81 | __, x = await ak.event(button, 'x', filter=lambda __, x: x>100) 82 | print(f'button.x is now {x}') 83 | 84 | # waits for either 5 seconds to elapse or a button to be pressed. 85 | # i.e. waits at most 5 seconds for a button to be pressed 86 | tasks = await ak.wait_any( 87 | ak.sleep(5), 88 | ak.event(button, 'on_press'), 89 | ) 90 | print("Timeout" if tasks[0].finished else "The button was pressed") 91 | 92 | # same as the above 93 | async with ak.move_on_after(5) as bg_task: 94 | await ak.event(button, 'on_press') 95 | print("Timeout" if bg_task.finished else "The button was pressed") 96 | 97 | # waits for both 5 seconds to elapse and a button to be pressed. 98 | tasks = await ak.wait_all( 99 | ak.sleep(5), 100 | ak.event(button, 'on_press'), 101 | ) 102 | 103 | # nest as you want. 104 | # waits for a button to be pressed, and either 5 seconds to elapse or 'other_async_func' to complete. 105 | tasks = await ak.wait_all( 106 | ak.event(button, 'on_press'), 107 | ak.wait_any( 108 | ak.sleep(5), 109 | other_async_func(), 110 | ), 111 | ) 112 | child_tasks = tasks[1].result 113 | print("5 seconds elapsed" if child_tasks[0].finished else "other_async_func has completed") 114 | 115 | ak.managed_start(some_task(some_button)) 116 | ``` 117 | 118 | For more details, read the [documentation](https://asyncgui.github.io/asynckivy/). 119 | 120 | ## Tested on 121 | 122 | - CPython 3.9 + Kivy 2.3 123 | - CPython 3.10 + Kivy 2.3 124 | - CPython 3.11 + Kivy 2.3 125 | - CPython 3.12 + Kivy 2.3 126 | - CPython 3.13 + Kivy 2.3 127 | 128 | ## Why this even exists 129 | 130 | Starting from version 2.0.0, Kivy supports two legitimate async libraries: [asyncio][asyncio] and [Trio][trio]. 131 | At first glance, developing another one might seem like [reinventing the wheel][reinventing]. 132 | Actually, I originally started this project just to learn how the async/await syntax works-- 133 | so at first, it really was 'reinventing the wheel'. 134 | 135 | But after experimenting with Trio in combination with Kivy for a while, 136 | I noticed that Trio isn't suitable for situations requiring fast reactions, such as handling touch events. 137 | The same applies to asyncio. 138 | You can confirm this by running `investigation/why_xxx_is_not_suitable_for_handling_touch_events.py` and rapidly clicking a mouse button. 139 | You'll notice that sometimes `'up'` isn't paired with a corresponding `'down'` in the console output. 140 | You'll also see that the touch coordinates aren't relative to a `RelativeLayout`, 141 | even though the widget receiving the touches belongs to it. 142 | 143 | The cause of these problems is that `trio.Event.set()` and `asyncio.Event.set()` don't *immediately* resume the tasks waiting for the `Event` to be set-- 144 | they merely schedule them to resume. 145 | The same is true for `nursery.start_soon()` and `asyncio.create_task()`. 146 | 147 | Trio and asyncio are async **I/O** libraries after all. 148 | They probably don't need to resume or start tasks immediately, but I believe this is essential for touch handling in Kivy. 149 | If touch events aren't processed promptly, their state might change before tasks even have a chance to handle them. 150 | Their core design might not be ideal for GUI applications in the first place. 151 | That's why I continue to develop the asynckivy library to this day. 152 | 153 | [asyncio]:https://docs.python.org/3/library/asyncio.html 154 | [trio]:https://trio.readthedocs.io/en/stable/ 155 | [reinventing]:https://en.wikipedia.org/wiki/Reinventing_the_wheel 156 | -------------------------------------------------------------------------------- /README_jp.md: -------------------------------------------------------------------------------- 1 | # AsyncKivy 2 | 3 | [Youtube](https://www.youtube.com/playlist?list=PLNdhqAjzeEGjTpmvNck4Uykps8s9LmRTJ) 4 | 5 | `asynckivy`はKivy用のlibraryで、 6 | よくあるasync libraryと同じでcallback関数だらけの醜いcodeを読みやすくしてくれます。 7 | 例えば 8 | 9 | 1. `A`を出力 10 | 1. 一秒待機 11 | 1. `B`を出力 12 | 1. buttonが押されるまで待機 13 | 1. `C`を出力 14 | 15 | といった事を普通にやろうとするとcodeは 16 | 17 | ```python 18 | from kivy.clock import Clock 19 | 20 | def what_you_want_to_do(button): 21 | print('A') 22 | 23 | def one_sec_later(__): 24 | print('B') 25 | button.bind(on_press=on_button_press) 26 | Clock.schedule_once(one_sec_later, 1) 27 | 28 | def on_button_press(button): 29 | button.unbind(on_press=on_button_press) 30 | print('C') 31 | 32 | what_you_want_to_do(...) 33 | ``` 34 | 35 | のように読みにくい物となりますが、`asynckivy`を用いることで 36 | 37 | ```python 38 | import asynckivy as ak 39 | 40 | async def what_you_want_to_do(button): 41 | print('A') 42 | await ak.sleep(1) 43 | print('B') 44 | await ak.event(button, 'on_press') 45 | print('C') 46 | 47 | ak.managed_start(what_you_want_to_do(...)) 48 | ``` 49 | 50 | と分かりやすく書けます。 51 | 52 | ## Install方法 53 | 54 | マイナーバージョンが変わった時は何らかの重要な互換性の無い変更が加えられた事を意味するので使う際はマイナーバージョンまでを固定してください。 55 | 56 | ```text 57 | poetry add asynckivy@~0.8 58 | pip install "asynckivy>=0.8,<0.9" 59 | ``` 60 | 61 | ## 使い方 62 | 63 | ```python 64 | import asynckivy as ak 65 | 66 | async def async_func(button): 67 | # 1秒待つ 68 | dt = await ak.sleep(1) 69 | print(f'{dt}秒経ちました') 70 | 71 | # buttonが押されるまで待つ 72 | await ak.event(button, 'on_press') 73 | 74 | # 'button.x'の値が変わるまで待つ 75 | __, x = await ak.event(button, 'x') 76 | print(f'button.x の現在の値は {x} です') 77 | 78 | # 'button.x'の値が100を超えるまで待つ 79 | if button.x <= 100: 80 | __, x = await ak.event(button, 'x', filter=lambda __, x: x>100) 81 | print(f'button.x の現在の値は {x} です') 82 | 83 | # buttonが押される か 5秒経つまで待つ (その1) 84 | tasks = await ak.wait_any( 85 | ak.event(button, 'on_press'), 86 | ak.sleep(5), 87 | ) 88 | print("buttonが押されました" if tasks[0].finished else "5秒経ちました") 89 | 90 | # buttonが押される か 5秒経つまで待つ (その2) 91 | async with ak.move_on_after(5) as bg_task: 92 | await ak.event(button, 'on_press') 93 | print("5秒経ちました" if bg_task.finished else "buttonが押されました") 94 | 95 | # buttonが押され なおかつ 5秒経つまで待つ 96 | tasks = await ak.wait_all( 97 | ak.event(button, 'on_press'), 98 | ak.sleep(5), 99 | ) 100 | 101 | # buttonが押され なおかつ [5秒経つ か 'other_async_func'が完了する] まで待つ 102 | tasks = await ak.wait_all( 103 | ak.event(button, 'on_press'), 104 | ak.wait_any( 105 | ak.sleep(5), 106 | other_async_func(), 107 | ), 108 | ) 109 | child_tasks = tasks[1].result 110 | print("5秒経ちました" if child_tasks[0].finished else "other_async_funcが完了しました") 111 | 112 | ak.managed_start(async_func(a_button)) 113 | ``` 114 | 115 | より詳しい使い方は[こちら](https://asyncgui.github.io/asynckivy/)をご覧ください。 116 | -------------------------------------------------------------------------------- /examples/README.txt: -------------------------------------------------------------------------------- 1 | The examples provided here are fairly simple. 2 | If you want to see more complex or real-world usages of asynckivy, visit https://github.com/gottadiveintopython/kivyx2. 3 | -------------------------------------------------------------------------------- /examples/asking_questions.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | import enum 3 | 4 | from kivy.clock import Clock 5 | from kivy.app import App 6 | from kivy.lang import Builder 7 | from kivy.factory import Factory as F 8 | import asynckivy as ak 9 | 10 | KV_CODE = ''' 11 | : 12 | size_hint: 0.8, 0.4 13 | BoxLayout: 14 | padding: '10dp' 15 | spacing: '10dp' 16 | orientation: 'vertical' 17 | Label: 18 | id: label 19 | Button: 20 | id: ok_button 21 | text: 'OK' 22 | 23 | : 24 | size_hint: 0.8, 0.4 25 | BoxLayout: 26 | padding: '10dp' 27 | spacing: '10dp' 28 | orientation: 'vertical' 29 | Label: 30 | id: label 31 | BoxLayout: 32 | spacing: '10dp' 33 | Button: 34 | id: no_button 35 | text: 'No' 36 | Button: 37 | id: yes_button 38 | text: 'Yes' 39 | 40 | : 41 | size_hint: 0.8, 0.4 42 | BoxLayout: 43 | padding: '10dp' 44 | spacing: '10dp' 45 | orientation: 'vertical' 46 | Label: 47 | id: label 48 | TextInput: 49 | id: textinput 50 | multiline: False 51 | BoxLayout: 52 | spacing: '10dp' 53 | Button: 54 | id: cancel_button 55 | text: 'Cancel' 56 | Button: 57 | id: ok_button 58 | text: 'OK' 59 | 60 | Widget: 61 | ''' 62 | 63 | 64 | class CauseOfDismissal(enum.Enum): 65 | AUTO = enum.auto() # ModalView.auto_dismiss or ModalView.dismiss() 66 | YES = enum.auto() 67 | NO = enum.auto() 68 | OK = enum.auto() 69 | CANCEL = enum.auto() 70 | UNKNOWN = enum.auto() # potentially a bug 71 | 72 | 73 | C = CauseOfDismissal 74 | 75 | 76 | async def show_message_box(msg, *, _cache=[]) -> T.Awaitable[C]: 77 | dialog = _cache.pop() if _cache else F.MessageBox() 78 | label = dialog.ids.label 79 | ok_button = dialog.ids.ok_button 80 | 81 | label.text = msg 82 | try: 83 | dialog.open() 84 | tasks = await ak.wait_any( 85 | ak.event(dialog, 'on_pre_dismiss'), 86 | ak.event(ok_button, 'on_press'), 87 | ) 88 | for task, cause in zip(tasks, (C.AUTO, C.OK, )): 89 | if task.finished: 90 | return cause 91 | return C.UNKNOWN 92 | finally: 93 | dialog.dismiss() 94 | Clock.schedule_once( 95 | lambda dt: _cache.append(dialog), 96 | dialog._anim_duration + 0.1, 97 | ) 98 | 99 | 100 | async def ask_yes_no_question(question, *, _cache=[]) -> T.Awaitable[C]: 101 | dialog = _cache.pop() if _cache else F.YesNoDialog() 102 | label = dialog.ids.label 103 | no_button = dialog.ids.no_button 104 | yes_button = dialog.ids.yes_button 105 | 106 | label.text = question 107 | try: 108 | dialog.open() 109 | tasks = await ak.wait_any( 110 | ak.event(dialog, 'on_pre_dismiss'), 111 | ak.event(no_button, 'on_press'), 112 | ak.event(yes_button, 'on_press'), 113 | ) 114 | for task, cause in zip(tasks, (C.AUTO, C.NO, C.YES, )): 115 | if task.finished: 116 | return cause 117 | return C.UNKNOWN 118 | finally: 119 | dialog.dismiss() 120 | Clock.schedule_once( 121 | lambda dt: _cache.append(dialog), 122 | dialog._anim_duration + 0.1, 123 | ) 124 | 125 | 126 | async def ask_input(msg, *, input_filter, input_type, _cache=[]) -> T.Awaitable[T.Tuple[C, str]]: 127 | dialog = _cache.pop() if _cache else F.InputDialog() 128 | label = dialog.ids.label 129 | textinput = dialog.ids.textinput 130 | cancel_button = dialog.ids.cancel_button 131 | ok_button = dialog.ids.ok_button 132 | 133 | label.text = msg 134 | textinput.input_filter = input_filter 135 | textinput.input_type = input_type 136 | textinput.focus = True 137 | try: 138 | dialog.open() 139 | tasks = await ak.wait_any( 140 | ak.event(dialog, 'on_pre_dismiss'), 141 | ak.event(cancel_button, 'on_press'), 142 | ak.event(ok_button, 'on_press'), 143 | ak.event(textinput, 'on_text_validate'), 144 | ) 145 | for task, cause in zip(tasks, (C.AUTO, C.CANCEL, C.OK, C.OK, )): 146 | if task.finished: 147 | return cause, textinput.text 148 | return C.UNKNOWN, textinput.text 149 | finally: 150 | dialog.dismiss() 151 | Clock.schedule_once( 152 | lambda dt: _cache.append(dialog), 153 | dialog._anim_duration + 0.1, 154 | ) 155 | 156 | 157 | class SampleApp(App): 158 | def build(self): 159 | return Builder.load_string(KV_CODE) 160 | 161 | def on_start(self): 162 | ak.managed_start(self.main()) 163 | 164 | async def main(self): 165 | await ak.n_frames(2) 166 | 167 | msg = "Do you like Kivy?" 168 | cause = await ask_yes_no_question(msg) 169 | print(f"{msg!r} --> {cause.name}") 170 | 171 | msg = "How long have you been using Kivy (in years) ?" 172 | while True: 173 | cause, years = await ask_input(msg, input_filter='int', input_type='number') 174 | if cause is C.OK: 175 | if years: 176 | print(f"{msg!r} --> {years} years") 177 | break 178 | else: 179 | await show_message_box("The text box is empty. Try again.") 180 | continue 181 | else: 182 | print(f"{msg!r} --> {cause.name}") 183 | break 184 | 185 | msg = "The armor I used to seal my all too powerful strength is now broken." 186 | cause = await show_message_box(msg) 187 | print(f"{msg!r} --> {cause.name}") 188 | 189 | 190 | if __name__ == '__main__': 191 | SampleApp().run() 192 | -------------------------------------------------------------------------------- /examples/asking_questions_simplified.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | 3 | from kivy.clock import Clock 4 | from kivy.app import App 5 | from kivy.lang import Builder 6 | from kivy.factory import Factory as F 7 | import asynckivy as ak 8 | 9 | KV_CODE = ''' 10 | : 11 | size_hint: 0.8, 0.4 12 | BoxLayout: 13 | padding: '10dp' 14 | spacing: '10dp' 15 | orientation: 'vertical' 16 | Label: 17 | id: label 18 | BoxLayout: 19 | spacing: '10dp' 20 | Button: 21 | id: no_button 22 | text: 'No' 23 | Button: 24 | id: yes_button 25 | text: 'Yes' 26 | 27 | Widget: 28 | ''' 29 | 30 | 31 | async def ask_yes_no_question(question, *, _cache=[]) -> T.Awaitable[str]: 32 | dialog = _cache.pop() if _cache else F.YesNoDialog() 33 | no_button = dialog.ids.no_button 34 | yes_button = dialog.ids.yes_button 35 | 36 | dialog.ids.label.text = question 37 | try: 38 | dialog.open() 39 | tasks = await ak.wait_any( 40 | ak.event(dialog, 'on_pre_dismiss'), 41 | ak.event(no_button, 'on_press'), 42 | ak.event(yes_button, 'on_press'), 43 | ) 44 | for task, r in zip(tasks, ("AUTO", "NO", "YES", )): 45 | if task.finished: 46 | return r 47 | return "UNKNOWN" 48 | finally: 49 | dialog.dismiss() 50 | Clock.schedule_once( 51 | lambda dt: _cache.append(dialog), 52 | dialog._anim_duration + 0.1, 53 | ) 54 | 55 | 56 | class SampleApp(App): 57 | def build(self): 58 | return Builder.load_string(KV_CODE) 59 | 60 | def on_start(self): 61 | ak.managed_start(self.main()) 62 | 63 | async def main(self): 64 | await ak.n_frames(2) 65 | 66 | msg = "Do you like Kivy?" 67 | res = await ask_yes_no_question(msg) 68 | print(f"{msg!r} --> {res}") 69 | 70 | await ak.sleep(.5) 71 | 72 | msg = "Do you like Python?" 73 | res = await ask_yes_no_question(msg) 74 | print(f"{msg!r} --> {res}") 75 | 76 | 77 | if __name__ == '__main__': 78 | SampleApp().run() 79 | -------------------------------------------------------------------------------- /examples/changing_text_with_fade_transition.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A simple usecase of ``asynckivy.fade_transition()``. 3 | ''' 4 | 5 | from kivy.utils import get_random_color 6 | from kivy.app import App 7 | from kivy.uix.label import Label 8 | import asynckivy as ak 9 | 10 | 11 | class TestApp(App): 12 | def build(self): 13 | return Label(font_size=40) 14 | 15 | def on_start(self): 16 | ak.managed_start(self.main()) 17 | 18 | async def main(self): 19 | await ak.n_frames(4) 20 | label = self.root 21 | for text in ( 22 | 'Zen of Python', 23 | 'Beautiful is better than ugly.', 24 | 'Explicit is better than implicit.', 25 | 'Simple is better than complex.', 26 | 'Complex is better than complicated.', 27 | '', 28 | ): 29 | async with ak.fade_transition(label): 30 | label.text = text 31 | label.color = get_random_color() 32 | await ak.event(label, 'on_touch_down') 33 | 34 | 35 | if __name__ == '__main__': 36 | TestApp(title=r"fade_transition").run() 37 | -------------------------------------------------------------------------------- /examples/loop_animation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A simple loop-animation. 3 | ''' 4 | 5 | from kivy.app import App 6 | from kivy.uix.label import Label 7 | from kivy.utils import get_color_from_hex 8 | import asynckivy as ak 9 | 10 | 11 | class TestApp(App): 12 | 13 | def build(self): 14 | return Label(text='Hello', markup=True, font_size='80sp', 15 | outline_width=2, 16 | outline_color=get_color_from_hex('#FFFFFF'), 17 | color=get_color_from_hex('#000000'), 18 | ) 19 | 20 | def on_start(self): 21 | ak.managed_start(self.main()) 22 | 23 | async def main(self): 24 | # LOAD_FAST 25 | label = self.root 26 | sleep = ak.sleep 27 | 28 | await ak.n_frames(4) 29 | while True: 30 | label.outline_color = get_color_from_hex('#FFFFFF') 31 | label.text = 'Do' 32 | await sleep(.5) 33 | label.text = 'you' 34 | await sleep(.5) 35 | label.text = 'like' 36 | await sleep(.5) 37 | label.text = 'Kivy?' 38 | await sleep(2) 39 | 40 | label.outline_color = get_color_from_hex('#FF5555') 41 | label.text = 'Answer me!' 42 | await sleep(2) 43 | 44 | 45 | if __name__ == '__main__': 46 | TestApp(title=r"Do you like Kivy ?").run() 47 | -------------------------------------------------------------------------------- /examples/network_io.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Demonstrates how to perform an I/O operation in asynckivy. 3 | ''' 4 | import requests 5 | from kivy.app import App 6 | from kivy.uix.button import Button 7 | import asynckivy as ak 8 | 9 | 10 | class TestApp(App): 11 | 12 | def build(self): 13 | return Button(font_size='20sp') 14 | 15 | def on_start(self): 16 | ak.managed_start(self.main()) 17 | 18 | async def main(self): 19 | button = self.root 20 | button.text = 'start a http request' 21 | await ak.event(button, 'on_press') 22 | button.text = 'waiting for the server to respond...' 23 | res = await ak.run_in_thread(lambda: requests.get("https://httpbin.org/delay/2"), daemon=True) 24 | button.text = res.json()['headers']['User-Agent'] 25 | 26 | 27 | if __name__ == '__main__': 28 | TestApp().run() 29 | -------------------------------------------------------------------------------- /examples/painter2.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Painter 3 | ======= 4 | 5 | * can only handle one touch at a time 6 | ''' 7 | 8 | from functools import partial 9 | from kivy.uix.relativelayout import RelativeLayout 10 | from kivy.app import App 11 | from kivy.graphics import Line, Color 12 | from kivy.utils import get_random_color 13 | import asynckivy as ak 14 | 15 | 16 | class Painter(RelativeLayout): 17 | @staticmethod 18 | def accepts_touch(widget, touch) -> bool: 19 | return widget.collide_point(*touch.opos) and (not touch.is_mouse_scrolling) 20 | 21 | async def main(self): 22 | on_touch_down = partial(ak.event, self, 'on_touch_down', filter=self.accepts_touch, stop_dispatching=True) 23 | while True: 24 | __, touch = await on_touch_down() 25 | await self.draw_rect(touch) 26 | 27 | async def draw_rect(self, touch): 28 | # LOAD_FAST 29 | self_to_local = self.to_local 30 | ox, oy = self_to_local(*touch.opos) 31 | 32 | with self.canvas: 33 | Color(*get_random_color()) 34 | line = Line(width=2) 35 | 36 | async for __ in ak.rest_of_touch_events(self, touch, stop_dispatching=True): 37 | # Don't await anything during the loop 38 | x, y = self_to_local(*touch.pos) 39 | min_x, max_x = (x, ox) if x < ox else (ox, x) 40 | min_y, max_y = (y, oy) if y < oy else (oy, y) 41 | line.rectangle = (min_x, min_y, max_x - min_x, max_y - min_y, ) 42 | 43 | 44 | class SampleApp(App): 45 | def build(self): 46 | return Painter() 47 | 48 | def on_start(self): 49 | ak.managed_start(self.root.main()) 50 | 51 | 52 | if __name__ == "__main__": 53 | SampleApp(title='Painter').run() 54 | -------------------------------------------------------------------------------- /examples/painter3.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Painter 3 | ======= 4 | 5 | * can handle multiple touches simultaneously 6 | ''' 7 | 8 | from functools import cached_property, partial 9 | from kivy.uix.relativelayout import RelativeLayout 10 | from kivy.app import App 11 | import asynckivy as ak 12 | from kivy.graphics import Line, Color 13 | from kivy.utils import get_random_color 14 | 15 | 16 | class Painter(RelativeLayout): 17 | @cached_property 18 | def _ud_key(self): 19 | return 'Painter.' + str(self.uid) 20 | 21 | @staticmethod 22 | def accepts_touch(self, touch) -> bool: 23 | return self.collide_point(*touch.opos) and (not touch.is_mouse_scrolling) and (self._ud_key not in touch.ud) 24 | 25 | async def main(self): 26 | on_touch_down = partial(ak.event, self, 'on_touch_down', filter=self.accepts_touch, stop_dispatching=True) 27 | async with ak.open_nursery() as nursery: 28 | while True: 29 | __, touch = await on_touch_down() 30 | touch.ud[self._ud_key] = True 31 | nursery.start(self.draw_rect(touch)) 32 | 33 | async def draw_rect(self, touch): 34 | # LOAD_FAST 35 | self_to_local = self.to_local 36 | ox, oy = self_to_local(*touch.opos) 37 | 38 | with self.canvas: 39 | Color(*get_random_color()) 40 | line = Line(width=2) 41 | 42 | async for __ in ak.rest_of_touch_events(self, touch, stop_dispatching=True): 43 | # Don't await anything during the loop 44 | x, y = self_to_local(*touch.pos) 45 | min_x, max_x = (x, ox) if x < ox else (ox, x) 46 | min_y, max_y = (y, oy) if y < oy else (oy, y) 47 | line.rectangle = (min_x, min_y, max_x - min_x, max_y - min_y, ) 48 | 49 | 50 | class SampleApp(App): 51 | def build(self): 52 | return Painter() 53 | 54 | def on_start(self): 55 | ak.managed_start(self.root.main()) 56 | 57 | 58 | if __name__ == "__main__": 59 | SampleApp(title='Painter').run() 60 | -------------------------------------------------------------------------------- /examples/painter4.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Painter 3 | ======= 4 | 5 | * can handle multiple touches simultaneously 6 | * uses 'event_freq' and 'move_on_when' instead of 'rest_of_touch_events' 7 | ''' 8 | 9 | from functools import cached_property, partial 10 | from kivy.uix.relativelayout import RelativeLayout 11 | from kivy.app import App 12 | import asynckivy as ak 13 | from kivy.graphics import Line, Color 14 | from kivy.utils import get_random_color 15 | from kivy.core.window import Window 16 | 17 | 18 | class Painter(RelativeLayout): 19 | @cached_property 20 | def _ud_key(self): 21 | return 'Painter.' + str(self.uid) 22 | 23 | @staticmethod 24 | def accepts_touch(self, touch) -> bool: 25 | return self.collide_point(*touch.opos) and (not touch.is_mouse_scrolling) and (self._ud_key not in touch.ud) 26 | 27 | async def main(self): 28 | on_touch_down = partial(ak.event, self, 'on_touch_down', filter=self.accepts_touch, stop_dispatching=True) 29 | async with ak.open_nursery() as nursery: 30 | while True: 31 | __, touch = await on_touch_down() 32 | touch.ud[self._ud_key] = True 33 | nursery.start(self.draw_rect(touch)) 34 | 35 | async def draw_rect(self, touch): 36 | # LOAD_FAST 37 | self_to_local = self.to_local 38 | ox, oy = self_to_local(*touch.opos) 39 | 40 | with self.canvas: 41 | Color(*get_random_color()) 42 | line = Line(width=2) 43 | 44 | def filter(w, t, touch=touch): 45 | return t is touch 46 | async with ( 47 | ak.move_on_when(ak.event(Window, 'on_touch_up', filter=filter)), 48 | ak.event_freq(self, 'on_touch_move', filter=filter, stop_dispatching=True) as on_touch_move, 49 | ): 50 | while True: 51 | await on_touch_move() 52 | x, y = self_to_local(*touch.pos) 53 | min_x, max_x = (x, ox) if x < ox else (ox, x) 54 | min_y, max_y = (y, oy) if y < oy else (oy, y) 55 | line.rectangle = (min_x, min_y, max_x - min_x, max_y - min_y, ) 56 | 57 | 58 | class SampleApp(App): 59 | def build(self): 60 | return Painter() 61 | 62 | def on_start(self): 63 | ak.managed_start(self.root.main()) 64 | 65 | 66 | if __name__ == "__main__": 67 | SampleApp(title='Painter').run() 68 | -------------------------------------------------------------------------------- /examples/popping_widget.py: -------------------------------------------------------------------------------- 1 | from contextlib import nullcontext 2 | from functools import partial 3 | 4 | from kivy.config import Config 5 | Config.set('modules', 'showborder', '') 6 | from kivy.app import App 7 | from kivy.lang import Builder 8 | from kivy.graphics import Rotate, Translate 9 | from asynckivy import anim_with_dt_et_ratio, transform, suppress_event 10 | 11 | 12 | ignore_touch_down = partial(suppress_event, event_name='on_touch_down', filter=lambda w, t: w.collide_point(*t.opos)) 13 | ''' 14 | .. code-block:: 15 | 16 | with ignore_touch_down(widget): 17 | ... 18 | 19 | Returns a context manager that causes a specified ``widget`` to ignore ``on_touch_down`` events that intersect with it. 20 | This can be particularly useful when you need to disable touch interaction for a widget without altering its 21 | appearance. (Setting the ``disabled`` property to True might alter the appearance.) 22 | ''' 23 | 24 | 25 | degrees_per_second = float 26 | 27 | 28 | async def pop_widget(widget, *, height=300., duration=1., rotation_speed: degrees_per_second=360., ignore_touch=False): 29 | with ignore_touch_down(widget) if ignore_touch else nullcontext(), transform(widget) as ig: # <- InstructionGroup 30 | translate = Translate() 31 | rotate = Rotate(origin=widget.center) 32 | ig.add(translate) 33 | ig.add(rotate) 34 | async for dt, et, p in anim_with_dt_et_ratio(base=duration / 2.): 35 | p -= 1. 36 | translate.y = (-(p * p) + 1.) * height 37 | rotate.angle = et * rotation_speed 38 | if p >= 1.: 39 | break 40 | 41 | 42 | KV_CODE = r''' 43 | #:import ak asynckivy 44 | #:import pop_widget __main__.pop_widget 45 | 46 | : 47 | size_hint_y: None 48 | height: '100dp' 49 | font_size: '100sp' 50 | on_press: 51 | ak.managed_start(pop_widget(self, ignore_touch=True)) 52 | 53 | BoxLayout: 54 | spacing: '20dp' 55 | padding: '20dp' 56 | CustomButton: 57 | text: 'A' 58 | CustomButton: 59 | pos_hint: {'y': .1, } 60 | text: 'B' 61 | CustomButton: 62 | pos_hint: {'y': .2, } 63 | text: 'C' 64 | ''' 65 | 66 | 67 | class SampleApp(App): 68 | def build(self): 69 | return Builder.load_string(KV_CODE) 70 | 71 | 72 | if __name__ == '__main__': 73 | SampleApp(title='popping widget').run() 74 | -------------------------------------------------------------------------------- /examples/progress_spinner.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from kivy.app import App 3 | from kivy.graphics import Line, Color, InstructionGroup 4 | from kivy.uix.widget import Widget 5 | import asynckivy as ak 6 | 7 | 8 | async def progress_spinner( 9 | *, draw_target: InstructionGroup, center, radius, line_width=10, color=(1, 1, 1, 1, ), min_arc_angle=40, 10 | speed=1.0): 11 | 12 | BS = 40.0 # base speed (in degrees) 13 | AS = 360.0 - min_arc_angle * 2 14 | get_next_start = itertools.accumulate(itertools.cycle((BS, BS, BS + AS, BS, )), initial=0).__next__ 15 | get_next_stop = itertools.accumulate(itertools.cycle((BS + AS, BS, BS, BS, )), initial=min_arc_angle).__next__ 16 | d = 0.4 / speed 17 | start = get_next_start() 18 | stop = get_next_stop() 19 | draw_target.add(color_inst := Color(*color)) 20 | draw_target.add(line_inst := Line(width=line_width)) 21 | try: 22 | line_inst.circle = (*center, radius, start, stop) 23 | while True: 24 | next_start = get_next_start() 25 | next_stop = get_next_stop() 26 | async for sta, sto in ak.interpolate_seq((start, stop), (next_start, next_stop), duration=d): 27 | line_inst.circle = (*center, radius, sta, sto) 28 | start = next_start 29 | stop = next_stop 30 | finally: 31 | draw_target.remove(line_inst) 32 | draw_target.remove(color_inst) 33 | 34 | 35 | class TestApp(App): 36 | def build(self): 37 | return Widget() 38 | 39 | def on_start(self): 40 | ak.managed_start(self.main()) 41 | 42 | async def main(self): 43 | await ak.n_frames(4) 44 | root = self.root 45 | await progress_spinner( 46 | draw_target=root.canvas, 47 | center=root.center, 48 | radius=min(root.size) * 0.4, 49 | ) 50 | 51 | 52 | if __name__ == '__main__': 53 | TestApp(title="Progress Spinner").run() 54 | -------------------------------------------------------------------------------- /examples/skippable_animation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Skippable Animation 3 | =================== 4 | ''' 5 | 6 | from kivy.app import App 7 | from kivy.lang import Builder 8 | import asynckivy as ak 9 | 10 | 11 | KV_CODE = r''' 12 | RelativeLayout: 13 | Label: 14 | text: 'This animation can be skipped by touching the screen' 15 | Label: 16 | id: label 17 | size_hint: None, None 18 | size: self.texture_size 19 | text: 'Label' 20 | font_size: '80sp' 21 | ''' 22 | 23 | 24 | class TestApp(App): 25 | def build(self): 26 | return Builder.load_string(KV_CODE) 27 | 28 | def on_start(self): 29 | ak.managed_start(self.main()) 30 | 31 | async def main(self): 32 | from asynckivy import wait_any, event, anim_attrs 33 | root = self.root 34 | label = root.ids.label.__self__ 35 | while True: 36 | await wait_any( 37 | event(root, 'on_touch_down'), 38 | anim_attrs(label, right=root.width), 39 | ) 40 | label.right = root.width 41 | await wait_any( 42 | event(root, 'on_touch_down'), 43 | anim_attrs(label, top=root.height), 44 | ) 45 | label.top = root.height 46 | await wait_any( 47 | event(root, 'on_touch_down'), 48 | anim_attrs(label, x=0), 49 | ) 50 | label.x = 0 51 | await wait_any( 52 | event(root, 'on_touch_down'), 53 | anim_attrs(label, y=0), 54 | ) 55 | label.y = 0 56 | 57 | 58 | if __name__ == '__main__': 59 | TestApp(title="Skippable Animation").run() 60 | -------------------------------------------------------------------------------- /examples/smoothing.py: -------------------------------------------------------------------------------- 1 | from random import random, randint 2 | from kivy.event import EventDispatcher 3 | from kivy.graphics import Color, Rectangle, CanvasBase 4 | from kivy.properties import NumericProperty, ReferenceListProperty, ColorProperty 5 | from kivy.app import App 6 | from kivy.uix.widget import Widget 7 | import asynckivy as ak 8 | 9 | 10 | class AnimatedRectangle(EventDispatcher): 11 | x = NumericProperty() 12 | y = NumericProperty() 13 | pos = ReferenceListProperty(x, y) 14 | width = NumericProperty() 15 | height = NumericProperty() 16 | size = ReferenceListProperty(width, height) 17 | color = ColorProperty("#FFFFFFFF") 18 | 19 | def __init__(self, **kwargs): 20 | super().__init__(**kwargs) 21 | self.canvas = canvas = CanvasBase() 22 | with canvas: 23 | ak.smooth_attr((self, "color"), (Color(self.color), "rgba"), min_diff=0.02, speed=4) 24 | rect = Rectangle(pos=self.pos, size=self.size) 25 | ak.smooth_attr((self, "pos"), (rect, "pos")) 26 | ak.smooth_attr((self, "size"), (rect, "size")) 27 | 28 | 29 | class SampleApp(App): 30 | def build(self): 31 | root = Widget() 32 | rect = AnimatedRectangle(width=160, height=160) 33 | root.canvas.add(rect.canvas) 34 | 35 | def on_touch_down(_, touch): 36 | rect.pos = touch.pos 37 | rect.color = (random(), random(), random(), 1) 38 | rect.width = randint(50, 200) 39 | rect.height = randint(50, 200) 40 | root.bind(on_touch_down=on_touch_down) 41 | return root 42 | 43 | 44 | if __name__ == '__main__': 45 | SampleApp().run() 46 | -------------------------------------------------------------------------------- /examples/swipe_to_delete.py: -------------------------------------------------------------------------------- 1 | ''' 2 | https://youtu.be/T5mZPIsK9-o 3 | ''' 4 | 5 | from functools import partial 6 | from kivy.app import App 7 | from kivy.lang import Builder 8 | from kivy.graphics import Translate 9 | from kivy.uix.button import Button 10 | import asynckivy as ak 11 | 12 | 13 | def remove_child(layout, child): 14 | layout.remove_widget(child) 15 | 16 | 17 | async def enable_swipe_to_delete(target_layout, *, swipe_distance=400., delete_action=remove_child): 18 | ''' 19 | Enables swipe-to-delete functionality for a layout. While enabled, the API blocks all touch 20 | events that intersect with the layout, meaning that if there is a button inside the layout, 21 | the user cannot press it. 22 | 23 | :param delete_action: You can replace the default deletion action by passing a custom function. 24 | ''' 25 | layout = target_layout.__self__ 26 | se = partial(ak.suppress_event, layout, filter=lambda w, t: w.collide_point(*t.pos)) 27 | with se("on_touch_down"), se("on_touch_move"), se("on_touch_up"): 28 | while True: 29 | __, touch = await ak.event(layout, "on_touch_down") 30 | # 'layout.to_local()' here is not necessary for this example to work because the 'layout' is an 31 | # instance of BoxLayout, and the BoxLayout is not a relative-type widget. 32 | ox, oy = layout.to_local(*touch.opos) 33 | for c in layout.children: 34 | if c.collide_point(ox, oy): 35 | break 36 | else: 37 | continue 38 | try: 39 | ox = touch.ox 40 | with ak.transform(c) as ig: 41 | ig.add(translate := Translate()) 42 | async for __ in ak.rest_of_touch_events(layout, touch): 43 | translate.x = dx = touch.x - ox 44 | c.opacity = 1.0 - abs(dx) / swipe_distance 45 | if c.opacity < 0.3: 46 | delete_action(layout, c) 47 | finally: 48 | c.opacity = 1.0 49 | 50 | 51 | KV_CODE = r''' 52 | BoxLayout: 53 | spacing: '10dp' 54 | padding: '10dp' 55 | orientation: 'vertical' 56 | Switch: 57 | id: switch 58 | active: False 59 | size_hint_y: None 60 | height: '50dp' 61 | ScrollView: 62 | BoxLayout: 63 | id: container 64 | orientation: 'vertical' 65 | size_hint_y: None 66 | height: self.minimum_height 67 | spacing: '10dp' 68 | ''' 69 | 70 | 71 | class SampleApp(App): 72 | def build(self): 73 | root = Builder.load_string(KV_CODE) 74 | add_widget = root.ids.container.add_widget 75 | for i in range(20): 76 | add_widget(Button(text=str(i), size_hint_y=None, height='50dp')) 77 | return root 78 | 79 | def on_start(self): 80 | ak.managed_start(self.main()) 81 | 82 | async def main(self): 83 | ids = self.root.ids 84 | switch = ids.switch 85 | container = ids.container 86 | while True: 87 | await ak.event(switch, 'active', filter=lambda _, active: active) 88 | async with ak.run_as_main(ak.event(switch, 'active')): 89 | await enable_swipe_to_delete(container) 90 | 91 | 92 | if __name__ == '__main__': 93 | SampleApp(title='Swipe to Delete').run() 94 | -------------------------------------------------------------------------------- /examples/swipe_to_delete_ver_rv.py: -------------------------------------------------------------------------------- 1 | from kivy.app import App 2 | from kivy.lang import Builder 3 | import asynckivy as ak 4 | from swipe_to_delete import enable_swipe_to_delete 5 | 6 | 7 | KV_CODE = r''' 8 | BoxLayout: 9 | spacing: '10dp' 10 | padding: '10dp' 11 | orientation: 'vertical' 12 | Switch: 13 | id: switch 14 | active: False 15 | size_hint_y: None 16 | height: '50dp' 17 | RecycleView: 18 | data: [{'text': str(i), } for i in range(100)] 19 | viewclass: 'Button' 20 | RecycleBoxLayout: 21 | id: container 22 | orientation: 'vertical' 23 | size_hint_y: None 24 | height: self.minimum_height 25 | spacing: '10dp' 26 | default_size_hint: 1, None 27 | default_height: '50dp' 28 | ''' 29 | 30 | 31 | def remove_corresponding_data(recyclelayout, view_widget): 32 | recyclelayout.recycleview.data.pop(recyclelayout.get_view_index_at(view_widget.center)) 33 | 34 | 35 | class SampleApp(App): 36 | def build(self): 37 | return Builder.load_string(KV_CODE) 38 | 39 | def on_start(self): 40 | ak.managed_start(self.main()) 41 | 42 | async def main(self): 43 | ids = self.root.ids 44 | switch = ids.switch 45 | container = ids.container 46 | while True: 47 | await ak.event(switch, 'active', filter=lambda _, active: active) 48 | async with ak.run_as_main(ak.event(switch, 'active')): 49 | await enable_swipe_to_delete(container, delete_action=remove_corresponding_data) 50 | 51 | 52 | if __name__ == '__main__': 53 | SampleApp(title='Swipe to Delete (RecycleView)').run() 54 | -------------------------------------------------------------------------------- /examples/the_longer_you_press_the_widget_the_higher_it_hops.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from kivy.metrics import cm 4 | from kivy.app import App 5 | from kivy.lang import Builder 6 | from kivy.graphics import Translate, Scale 7 | import asynckivy as ak 8 | 9 | 10 | GRAVITY = -9.80665 * cm(100) # gravitational acceleration in pixels/second**2 11 | ignore_touch_down = partial( 12 | ak.suppress_event, event_name='on_touch_down', filter=lambda w, t: w.collide_point(*t.opos)) 13 | 14 | 15 | async def bounce_widget(widget, *, scale_x_max=3.0, gravity=0.2): 16 | ''' 17 | :param gravity: A gravity coefficient. A higher value makes the widget fall faster. 18 | ''' 19 | import asynckivy as ak 20 | 21 | if scale_x_max <= 1.0: 22 | raise ValueError(f"'scale_x_max' must be greater than 1.0. (was {scale_x_max})") 23 | if gravity <= 0: 24 | raise ValueError(f"'gravity' must be greater than 0. (was {gravity})") 25 | widget = widget.__self__ 26 | with ignore_touch_down(widget), ak.transform(widget) as ig: 27 | # phase 1: Widget becomes wider and shorter while it being pressed. 28 | scale = Scale(origin=(widget.center_x, widget.y)) 29 | ig.add(scale) 30 | async with ak.run_as_secondary( 31 | ak.anim_attrs(scale, x=scale_x_max, y=1.0 / scale_x_max, duration=0.25 * scale_x_max)): 32 | await ak.event(widget, 'on_release') 33 | 34 | # phase 2: Widget becomes thiner and taller after it got released. 35 | scale_x = scale.x 36 | scale_y = scale.y 37 | await ak.anim_attrs(scale, x=scale_y, y=scale_x, duration=0.1) 38 | 39 | # phase 3: Widget bounces and returns to its original size. 40 | ig.insert(0, translate := Translate()) 41 | initial_velocity = scale_x ** 2 * 1000.0 42 | gravity = GRAVITY * gravity 43 | async with ak.wait_all_cm(ak.anim_attrs(scale, x=1.0, y=1.0, duration=0.1)): 44 | async for et in ak.anim_with_et(): 45 | translate.y = y = et * (initial_velocity + gravity * et) 46 | if y <= 0: 47 | break 48 | ig.remove(translate) 49 | 50 | # phase 4: Widget becomes wider and shorter on landing. 51 | await ak.anim_attrs(scale, x=(scale_x + 1.0) * 0.5, y=(scale_y + 1.0) * 0.5, duration=0.1) 52 | 53 | # phase 5: Widget returns to its original size. 54 | await ak.anim_attrs(scale, x=1.0, y=1.0, duration=0.1) 55 | 56 | 57 | KV_CODE = r''' 58 | #:import ak asynckivy 59 | #:import bounce_widget __main__.bounce_widget 60 | 61 | : 62 | always_release: True 63 | on_press: ak.managed_start(bounce_widget(self)) 64 | canvas: 65 | PushMatrix: 66 | Translate: 67 | xy: self.center 68 | Scale: 69 | xyz: (*self.size, 1.0, ) 70 | Color: 71 | rgba: (1, 1, 1, 1, ) 72 | Line: 73 | width: 0.01 74 | circle: 0, 0, 0.5 75 | Line: 76 | width: 0.01 77 | circle: 0.2, 0.18, 0.15 78 | Ellipse: 79 | pos: 0.2 - 0.01, 0.18 - 0.01 80 | size: 0.1, 0.1 81 | Line: 82 | width: 0.01 83 | circle: -0.2, 0.18, 0.15 84 | Ellipse: 85 | pos: -0.2 - 0.01, 0.18 - 0.01 86 | size: 0.1, 0.1 87 | PopMatrix: 88 | 89 | BoxLayout: 90 | spacing: '20dp' 91 | padding: '20dp' 92 | Widget: 93 | Puyo: 94 | size_hint: None, None 95 | size: '200dp', '170dp' 96 | Widget: 97 | ''' 98 | 99 | 100 | class SampleApp(App): 101 | def build(self): 102 | return Builder.load_string(KV_CODE) 103 | 104 | 105 | if __name__ == '__main__': 106 | SampleApp(title='The longer you press the widget, the higher it hops').run() 107 | -------------------------------------------------------------------------------- /examples/trio_move_on_after.py: -------------------------------------------------------------------------------- 1 | from kivy.app import App 2 | from kivy.uix.label import Label 3 | import asynckivy as ak 4 | 5 | 6 | class TestApp(App): 7 | 8 | def build(self): 9 | return Label(font_size="200sp") 10 | 11 | def on_start(self): 12 | ak.managed_start(self.main()) 13 | 14 | async def main(self): 15 | label = self.root 16 | await ak.n_frames(2) 17 | async with ak.move_on_after(3): 18 | while True: 19 | label.text = 'A' 20 | await ak.sleep(.4) 21 | label.text = 'B' 22 | await ak.sleep(.4) 23 | label.text = 'C' 24 | await ak.sleep(.4) 25 | label.text = "fin" 26 | label.italic = True 27 | 28 | 29 | if __name__ == '__main__': 30 | TestApp(title="trio.move_on_after()").run() 31 | -------------------------------------------------------------------------------- /examples/using_interpolate.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A simple usecase of ``asynckivy.interpolate()``. 3 | ''' 4 | 5 | from kivy.app import App 6 | from kivy.uix.label import Label 7 | import asynckivy as ak 8 | 9 | 10 | class TestApp(App): 11 | 12 | def build(self): 13 | return Label() 14 | 15 | def on_start(self): 16 | ak.managed_start(self.main()) 17 | 18 | async def main(self): 19 | label = self.root 20 | await ak.n_frames(4) 21 | async for font_size in ak.interpolate(start=0, end=300, duration=5, step=.1, transition='out_cubic'): 22 | label.font_size = font_size 23 | label.text = str(int(font_size)) 24 | 25 | 26 | if __name__ == '__main__': 27 | TestApp().run() 28 | -------------------------------------------------------------------------------- /investigation/compare_various_ways_to_repeat_sleeping.py: -------------------------------------------------------------------------------- 1 | from kivy.config import Config 2 | Config.set('graphics', 'maxfps', 0) 3 | from kivy.clock import Clock 4 | from kivy.app import App 5 | from kivy.lang import Builder 6 | import asynckivy as ak 7 | 8 | 9 | KV_CODE = ''' 10 | : 11 | group: 'aaa' 12 | font_size: '20sp' 13 | on_state: if args[1] == 'down': app.measure_fps(self.text) 14 | allow_no_selection: False 15 | 16 | BoxLayout: 17 | orientation: 'vertical' 18 | padding: 10 19 | spacing: 10 20 | MyToggleButton: 21 | text: 'schedule_interval' 22 | MyToggleButton: 23 | text:'sleep' 24 | MyToggleButton: 25 | text: 'repeat_sleeping' 26 | MyToggleButton: 27 | text: 'anim_with_dt' 28 | MyToggleButton: 29 | text: 'anim_with_dt_et' 30 | ''' 31 | 32 | 33 | class SampleApp(App): 34 | def build(self): 35 | self._tasks = [] 36 | return Builder.load_string(KV_CODE) 37 | 38 | def on_start(self): 39 | def print_fps(dt, print=print, get_fps=Clock.get_fps): 40 | print(get_fps(), 'fps') 41 | Clock.schedule_interval(print_fps, 1) 42 | 43 | def measure_fps(self, type): 44 | print('---- start measuring ----', type) 45 | async_func = globals()['ver_' + type] 46 | for task in self._tasks: 47 | task.cancel() 48 | self._tasks = [ak.start(async_func()) for __ in range(100)] 49 | 50 | 51 | async def ver_schedule_interval(): 52 | clock_event = Clock.schedule_interval(lambda __: None, 0) 53 | try: 54 | await ak.sleep_forever() 55 | finally: 56 | clock_event.cancel() 57 | 58 | 59 | async def ver_sleep(): 60 | sleep = ak.sleep 61 | while True: 62 | await sleep(0) 63 | 64 | 65 | async def ver_repeat_sleeping(): 66 | async with ak.repeat_sleeping(step=0) as sleep: 67 | while True: 68 | await sleep() 69 | 70 | 71 | async def ver_anim_with_dt(): 72 | async for dt in ak.anim_with_dt(): 73 | pass 74 | 75 | 76 | async def ver_anim_with_dt_et(): 77 | async for dt, et in ak.anim_with_dt_et(): 78 | pass 79 | 80 | 81 | if __name__ == '__main__': 82 | SampleApp().run() 83 | -------------------------------------------------------------------------------- /investigation/comparision_between_stateful_functions.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | 4 | def immediate_call(f): 5 | return f() 6 | 7 | 8 | def ver_list(p_value: list): 9 | value = p_value[0] + 1 10 | p_value[0] = value 11 | 12 | 13 | def ver_closure(): 14 | value = 0 15 | 16 | def inner(): 17 | nonlocal value 18 | _value = value + 1 19 | value = _value 20 | return inner 21 | 22 | 23 | class ver_attr: 24 | __slots__ = ('value', ) 25 | 26 | def __init__(self): 27 | self.value = 0 28 | 29 | def __call__(self): 30 | value = self.value + 1 31 | self.value = value 32 | 33 | 34 | @immediate_call 35 | def unittest_ver_list(): 36 | obj = partial(ver_list, [0]) 37 | assert obj.args[0][0] == 0 38 | obj() 39 | assert obj.args[0][0] == 1 40 | obj() 41 | assert obj.args[0][0] == 2 42 | 43 | 44 | @immediate_call 45 | def unittest_ver_closure(): 46 | obj = ver_closure() 47 | assert obj.__closure__[0].cell_contents == 0 48 | obj() 49 | assert obj.__closure__[0].cell_contents == 1 50 | obj() 51 | assert obj.__closure__[0].cell_contents == 2 52 | 53 | 54 | @immediate_call 55 | def unittest_ver_attr(): 56 | obj = ver_attr() 57 | assert obj.value == 0 58 | obj() 59 | assert obj.value == 1 60 | obj() 61 | assert obj.value == 2 62 | 63 | 64 | from timeit import timeit 65 | 66 | t_list = timeit(partial(ver_list, [0])) 67 | t_closure = timeit(ver_closure()) 68 | t_attr = timeit(ver_attr()) 69 | t_attr2 = timeit(partial(ver_attr.__call__, ver_attr())) 70 | print(f"{t_list = }") 71 | print(f"{t_closure = }") 72 | print(f"{t_attr = }") 73 | print(f"{t_attr2 = }") 74 | -------------------------------------------------------------------------------- /investigation/github_issue_11.py: -------------------------------------------------------------------------------- 1 | from kivy.lang import Builder 2 | from kivy.app import App 3 | import asynckivy as ak 4 | 5 | 6 | KV_CODE = ''' 7 | BoxLayout: 8 | orientation: 'vertical' 9 | Label: 10 | id: label 11 | font_size: 50 12 | Button: 13 | id: button 14 | font_size: 50 15 | ''' 16 | 17 | 18 | class TestApp(App): 19 | def build(self): 20 | return Builder.load_string(KV_CODE) 21 | 22 | def on_start(self): 23 | ak.start(self.main()) 24 | 25 | async def main(self): 26 | label = self.root.ids.label 27 | button = self.root.ids.button 28 | label.text = '--' 29 | button.text = 'start spinning' 30 | await ak.event(button, 'on_press') 31 | button.text = 'stop' 32 | tasks = await ak.wait_any( 33 | ak.event(button, 'on_press'), 34 | spinning(label), 35 | ) 36 | tasks[1].cancel() 37 | self.root.remove_widget(button) 38 | label.text = 'fin.' 39 | 40 | 41 | async def spinning(label): 42 | import itertools 43 | for stick in itertools.cycle(r'\ | / --'.split()): 44 | label.text = stick 45 | await ak.sleep(.1) 46 | 47 | 48 | if __name__ == '__main__': 49 | TestApp().run() 50 | -------------------------------------------------------------------------------- /investigation/speed_comparision_between_various_sleep_implementations.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from kivy.config import Config 3 | Config.set('graphics', 'maxfps', 0) 4 | from kivy.clock import Clock 5 | from kivy.app import App 6 | from kivy.lang import Builder 7 | from asyncgui import IBox, ISignal, Cancelled 8 | import asynckivy as ak 9 | 10 | import types 11 | 12 | KV_CODE = ''' 13 | : 14 | group: 'aaa' 15 | on_press: if self.state == 'down': app.measure_fps(self.text) 16 | 17 | BoxLayout: 18 | orientation: 'vertical' 19 | padding: 10 20 | spacing: 10 21 | MyToggleButton: 22 | text: 'box' 23 | MyToggleButton: 24 | text: 'box2' 25 | MyToggleButton: 26 | text: 'signal' 27 | MyToggleButton: 28 | text: 'ugly1' 29 | MyToggleButton: 30 | text: 'ugly2' 31 | MyToggleButton: 32 | text: 'ugly3' 33 | MyToggleButton: 34 | text: 'closure' 35 | ''' 36 | 37 | create_trigger = Clock.create_trigger 38 | 39 | 40 | async def sleep_ver_box(duration): 41 | box = IBox() 42 | create_trigger(box.put, duration, False, False)() 43 | return (await box.get())[0][0] 44 | 45 | 46 | async def sleep_ver_box2(duration): 47 | box = IBox() 48 | create_trigger(box.put, duration, False, False)() 49 | args, kwargs = await box.get() 50 | return args[0] 51 | 52 | 53 | async def sleep_ver_signal(duration): 54 | signal = ISignal() 55 | create_trigger(signal.set, duration, False, False)() 56 | await signal.wait() 57 | 58 | 59 | def _func1(ctx, duration, task): 60 | ctx['ce'] = ce = create_trigger(task._step, duration, False, False) 61 | ce() 62 | 63 | 64 | def _func2(ctx, duration, task): 65 | ctx[0] = ce = create_trigger(task._step, duration, False, False) 66 | ce() 67 | 68 | 69 | def _func3(ctx, duration, task): 70 | ctx.data = ce = create_trigger(task._step, duration, False, False) 71 | ce() 72 | 73 | 74 | class DataPassenger: 75 | __slots__ = ('data', ) 76 | 77 | 78 | @types.coroutine 79 | def sleep_ver_ugly1(duration): 80 | ctx = {} 81 | try: 82 | return (yield partial(_func1, ctx, duration))[0][0] 83 | except Cancelled: 84 | ctx['ce'].cancel() 85 | raise 86 | 87 | 88 | @types.coroutine 89 | def sleep_ver_ugly2(duration): 90 | ctx = [None, ] 91 | try: 92 | return (yield partial(_func2, ctx, duration))[0][0] 93 | except Cancelled: 94 | ctx[0].cancel() 95 | raise 96 | 97 | 98 | @types.coroutine 99 | def sleep_ver_ugly3(duration): 100 | ctx = DataPassenger() 101 | try: 102 | return (yield partial(_func3, ctx, duration))[0][0] 103 | except Cancelled: 104 | ctx.data.cancel() 105 | raise 106 | 107 | 108 | @types.coroutine 109 | def sleep_ver_closure(duration): 110 | clock_event = None 111 | 112 | def _inner(task): 113 | nonlocal clock_event 114 | clock_event = create_trigger(task._step, duration, False, False) 115 | clock_event() 116 | 117 | try: 118 | return (yield _inner)[0][0] 119 | except Cancelled: 120 | clock_event.cancel() 121 | raise 122 | 123 | 124 | def print_byte_code(): 125 | from dis import dis 126 | for key, obj in globals().items(): 127 | if not key.startswith("sleep_ver_"): 128 | continue 129 | print("---- byte code ----", key) 130 | dis(obj) 131 | print("\n\n") 132 | 133 | 134 | print_byte_code() 135 | 136 | 137 | async def repeat_sleep(sleep): 138 | while True: 139 | await sleep(0) 140 | 141 | 142 | class SampleApp(App): 143 | def build(self): 144 | self._tasks = [] 145 | return Builder.load_string(KV_CODE) 146 | 147 | def on_start(self): 148 | def print_fps(dt): 149 | print(Clock.get_fps(), 'fps') 150 | Clock.schedule_interval(print_fps, 1) 151 | 152 | def measure_fps(self, type): 153 | print('---- start measuring ----', type) 154 | sleep = globals()['sleep_ver_' + type] 155 | for task in self._tasks: 156 | task.cancel() 157 | self._tasks = [ak.start(repeat_sleep(sleep)) for __ in range(10)] 158 | 159 | 160 | if __name__ == '__main__': 161 | SampleApp().run() 162 | -------------------------------------------------------------------------------- /investigation/why_asyncio_is_not_suitable_for_handling_touch_events.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import kivy 3 | from kivy.app import async_runTouchApp 4 | from kivy.lang.builder import Builder 5 | kivy.require('2.0.0') 6 | 7 | 8 | KV_CODE = ''' 9 | BoxLayout: 10 | Widget: 11 | RelativeLayout: 12 | Widget: 13 | id: target 14 | ''' 15 | 16 | 17 | async def kivy_event(ed, name): 18 | def callback(*args, **kwargs): 19 | nonlocal parameter 20 | parameter = args 21 | ed.unbind_uid(name, bind_uid) 22 | event.set() 23 | 24 | parameter = None 25 | bind_uid = ed.fbind(name, callback) 26 | event = asyncio.Event() 27 | await event.wait() 28 | return parameter 29 | 30 | 31 | async def test_gui_event(widget): 32 | try: 33 | while True: 34 | __, touch = await kivy_event(widget, 'on_touch_down') 35 | print(touch.uid, 'down', touch.opos) 36 | __, touch = await kivy_event(widget, 'on_touch_up') 37 | print(touch.uid, 'up ', touch.pos) 38 | except asyncio.CancelledError: 39 | pass 40 | 41 | 42 | async def async_main(): 43 | root = Builder.load_string(KV_CODE) 44 | sub_task = asyncio.ensure_future(test_gui_event(root.ids.target)) 45 | 46 | async def main_task(): 47 | await async_runTouchApp(root, async_lib='asyncio') 48 | sub_task.cancel() 49 | 50 | await asyncio.gather(main_task(), sub_task) 51 | 52 | 53 | if __name__ == '__main__': 54 | asyncio.run(async_main()) 55 | -------------------------------------------------------------------------------- /investigation/why_asynckivy_is_suitable_for_handling_touch_events.py: -------------------------------------------------------------------------------- 1 | from kivy.app import runTouchApp 2 | from kivy.lang.builder import Builder 3 | import asynckivy 4 | 5 | KV_CODE = ''' 6 | BoxLayout: 7 | Widget: 8 | RelativeLayout: 9 | Widget: 10 | id: target 11 | ''' 12 | 13 | 14 | async def test_gui_event(widget): 15 | event = asynckivy.event 16 | while True: 17 | __, touch = await event(widget, 'on_touch_down') 18 | print(touch.uid, 'down', touch.opos) 19 | __, touch = await event(widget, 'on_touch_up') 20 | print(touch.uid, 'up ', touch.pos) 21 | 22 | 23 | def _test(): 24 | root = Builder.load_string(KV_CODE) 25 | asynckivy.start(test_gui_event(root.ids.target)) 26 | runTouchApp(root) 27 | 28 | 29 | if __name__ == '__main__': 30 | _test() 31 | -------------------------------------------------------------------------------- /investigation/why_trio_is_not_suitable_for_handling_touch_events.py: -------------------------------------------------------------------------------- 1 | import trio 2 | import kivy 3 | from kivy.app import async_runTouchApp 4 | from kivy.lang.builder import Builder 5 | kivy.require('2.0.0') 6 | 7 | 8 | KV_CODE = ''' 9 | BoxLayout: 10 | Widget: 11 | RelativeLayout: 12 | Widget: 13 | id: target 14 | ''' 15 | 16 | 17 | async def kivy_event(ed, name): 18 | def callback(*args, **kwargs): 19 | nonlocal parameter 20 | parameter = args 21 | ed.unbind_uid(name, bind_uid) 22 | event.set() 23 | 24 | parameter = None 25 | bind_uid = ed.fbind(name, callback) 26 | event = trio.Event() 27 | await event.wait() 28 | return parameter 29 | 30 | 31 | async def test_gui_event(widget): 32 | while True: 33 | __, touch = await kivy_event(widget, 'on_touch_down') 34 | print(touch.uid, 'down', touch.opos) 35 | __, touch = await kivy_event(widget, 'on_touch_up') 36 | print(touch.uid, 'up ', touch.pos) 37 | 38 | 39 | async def root_task(): 40 | root = Builder.load_string(KV_CODE) 41 | async with trio.open_nursery() as nursery: 42 | async def main_task(): 43 | await async_runTouchApp(root, async_lib='trio') 44 | nursery.cancel_scope.cancel() 45 | nursery.start_soon(test_gui_event, root.ids.target) 46 | nursery.start_soon(main_task) 47 | 48 | 49 | if __name__ == '__main__': 50 | trio.run(root_task) 51 | -------------------------------------------------------------------------------- /misc/notes.md: -------------------------------------------------------------------------------- 1 | # `fade_transition` は transitionの種類を選べるようにすべきか? 2 | 3 | fadeinの時とfadeoutの時の二種類のtransitionを指定する事になる。 4 | -------------------------------------------------------------------------------- /misc/todo.md: -------------------------------------------------------------------------------- 1 | - type hits & doc 2 | - callback と async/await のいいとこ取りの説明。(animationを例に用いる) 3 | -------------------------------------------------------------------------------- /misc/youtube_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asyncgui/asynckivy/aa2e29d45223e816bd447e179ada6ff295b44b45/misc/youtube_thumbnail.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "asynckivy" 3 | version = "0.8.2.dev0" 4 | description = "Async library for Kivy" 5 | authors = ["Nattōsai Mitō "] 6 | license = "MIT" 7 | readme = 'README.md' 8 | repository = 'https://github.com/asyncgui/asynckivy' 9 | homepage = 'https://github.com/asyncgui/asynckivy' 10 | keywords = ['async', 'kivy'] 11 | classifiers=[ 12 | 'Development Status :: 5 - Production/Stable', 13 | 'License :: OSI Approved :: MIT License', 14 | 'Intended Audience :: Developers', 15 | 'Programming Language :: Python', 16 | 'Programming Language :: Python :: 3.9', 17 | 'Programming Language :: Python :: 3.10', 18 | 'Programming Language :: Python :: 3.11', 19 | 'Programming Language :: Python :: 3.12', 20 | 'Programming Language :: Python :: 3.13', 21 | 'Topic :: Software Development :: Libraries', 22 | 'Operating System :: OS Independent', 23 | ] 24 | packages = [ 25 | { include = "asynckivy", from = "src" }, 26 | ] 27 | 28 | [tool.poetry.dependencies] 29 | python = "^3.9" 30 | asyncgui = ">=0.7.2,<0.9" 31 | 32 | [tool.poetry.group.dev.dependencies] 33 | pytest = "^7.0.1" 34 | flake8 = "^6.0.0" 35 | kivy = "~2.3" 36 | 37 | [tool.poetry.group.doc.dependencies] 38 | sphinx = "^7.0.0" 39 | sphinx-tabs = "^3.4.1" 40 | sphinx-autobuild = "^2021.3.14" 41 | furo = "^2023.9.10" 42 | 43 | [build-system] 44 | requires = ["poetry-core"] 45 | build-backend = "poetry.core.masonry.api" 46 | 47 | [tool.pytest.ini_options] 48 | xfail_strict = true 49 | addopts = "--maxfail=4 --strict-markers" 50 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # 0.9.0 2 | 3 | - Removes `MotionEventAlreadyEndedError`, and leaves the users to handle the `touch.time_end != -1` situation on their own. 4 | 5 | # Eventually 6 | 7 | - 適切な名前のファイルへコードを移す。例: `suppress_event` を `_event.py` へ。 8 | 9 | # Undetermind 10 | 11 | - `n_frames` をclosureを用いない実装にする。 12 | - `kivy.clock.Clock.frames` 13 | - `run_in_thread` が作るスレッドの名前を指定できるようにする。 14 | 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = C901,E252 3 | per-file-ignores= 4 | ./src/asynckivy/__init__.py:F401,F403 5 | ./examples/*.py:E402 6 | ./investigation/*.py:E402 7 | -------------------------------------------------------------------------------- /sphinx/_static/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asyncgui/asynckivy/aa2e29d45223e816bd447e179ada6ff295b44b45/sphinx/_static/dummy -------------------------------------------------------------------------------- /sphinx/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | import importlib.metadata 7 | 8 | # -- Project information ----------------------------------------------------- 9 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 10 | project = 'asynckivy' 11 | copyright = '2023, Mitō Nattōsai' 12 | author = 'Mitō Nattōsai' 13 | release = importlib.metadata.version(project) 14 | 15 | rst_epilog = """ 16 | .. |ja| replace:: 🇯🇵 17 | """ 18 | 19 | # -- General configuration --------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 21 | extensions = [ 22 | 'sphinx.ext.autodoc', 23 | 'sphinx.ext.intersphinx', 24 | # 'sphinx.ext.viewcode', 25 | 'sphinx.ext.githubpages', 26 | 'sphinx_tabs.tabs', 27 | 28 | ] 29 | templates_path = ['_templates'] 30 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 31 | language = 'en' 32 | add_module_names = False 33 | gettext_auto_build = False 34 | gettext_location = False 35 | 36 | 37 | # -- Options for HTML output ------------------------------------------------- 38 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 39 | html_theme = "furo" 40 | html_static_path = ['_static'] 41 | html_theme_options = { 42 | "top_of_page_button": "edit", 43 | } 44 | 45 | 46 | # -- Options for todo extension ---------------------------------------------- 47 | # https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration 48 | todo_include_todos = True 49 | 50 | 51 | # -- Options for intersphinx extension --------------------------------------- 52 | # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration 53 | intersphinx_mapping = { 54 | 'python': ('https://docs.python.org/3', None), 55 | 'kivy': ('https://kivy.org/doc/master', None), 56 | 'trio': ('https://trio.readthedocs.io/en/stable/', None), 57 | # 'trio_util': ('https://trio-util.readthedocs.io/en/latest/', None), 58 | 'asyncgui': ('https://asyncgui.github.io/asyncgui/', None), 59 | } 60 | 61 | 62 | # -- Options for autodoc extension --------------------------------------- 63 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration 64 | autodoc_mock_imports = ['kivy', ] 65 | # autodoc_default_options = { 66 | # 'members': True, 67 | # 'undoc-members': True, 68 | # 'no-show-inheritance': True, 69 | # } 70 | 71 | # -- Options for tabs extension --------------------------------------- 72 | # https://sphinx-tabs.readthedocs.io/en/latest/ 73 | sphinx_tabs_disable_tab_closing = True 74 | 75 | 76 | def modify_signature(app, what: str, name: str, obj, options, signature, return_annotation: str, 77 | prefix="asynckivy.", 78 | len_prefix=len("asynckivy."), 79 | group1={'rest_of_touch_events', }, 80 | ): 81 | if not name.startswith(prefix): 82 | return (signature, return_annotation, ) 83 | name = name[len_prefix:] 84 | if signature is not None: 85 | signature = signature.replace("kivy.animation.AnimationTransition.linear", "'linear'") 86 | if name in group1: 87 | print(f"Emit the signature of {name!r}") 88 | return ('(...)', return_annotation) 89 | elif name == "managed_start": 90 | return ("(aw: ~typing.Awaitable | ~asyncgui.Task, /)", return_annotation, ) 91 | 92 | return (signature, return_annotation, ) 93 | 94 | 95 | def modify_docstring(app, what: str, name: str, obj, options, lines, prefix="asynckivy.", 96 | len_prefix=len("asynckivy."), 97 | ): 98 | if not name.startswith(prefix): 99 | return 100 | name = name[len_prefix:] 101 | if name == "managed_start": 102 | from asynckivy._managed_start import __managed_start_doc__ 103 | lines.clear() 104 | lines.extend(__managed_start_doc__.split("\n")) 105 | return 106 | 107 | def setup(app): 108 | app.connect('autodoc-process-signature', modify_signature) 109 | app.connect('autodoc-process-docstring', modify_docstring) 110 | 111 | -------------------------------------------------------------------------------- /sphinx/index.rst: -------------------------------------------------------------------------------- 1 | AsyncKivy 2 | ========= 3 | 4 | ``asynckivy`` is an async library that saves you from ugly callback-style code, 5 | like most of async libraries do. 6 | Let's say you want to do: 7 | 8 | #. ``print('A')`` 9 | #. wait for 1sec 10 | #. ``print('B')``` 11 | #. wait for a button to be pressed 12 | #. ``print('C')`` 13 | 14 | in that order. 15 | Your code would look like this: 16 | 17 | .. code-block:: 18 | 19 | from kivy.clock import Clock 20 | 21 | def what_you_want_to_do(button): 22 | print('A') 23 | 24 | def one_sec_later(__): 25 | print('B') 26 | button.bind(on_press=on_button_press) 27 | Clock.schedule_once(one_sec_later, 1) 28 | 29 | def on_button_press(button): 30 | button.unbind(on_press=on_button_press) 31 | print('C') 32 | 33 | what_you_want_to_do(...) 34 | 35 | It's not easy to understand. 36 | If you use ``asynckivy``, the code above will become: 37 | 38 | .. code-block:: 39 | 40 | import asynckivy as ak 41 | 42 | async def what_you_want_to_do(button): 43 | print('A') 44 | await ak.sleep(1) 45 | print('B') 46 | await ak.event(button, 'on_press') 47 | print('C') 48 | 49 | ak.start(what_you_want_to_do(...)) 50 | 51 | You may also want to read the ``asyncgui``'s documentation as it is the foundation of this library. 52 | 53 | .. toctree:: 54 | :hidden: 55 | 56 | notes 57 | notes-ja 58 | reference 59 | 60 | * https://github.com/asyncgui/asyncgui 61 | * https://github.com/asyncgui/asynckivy 62 | -------------------------------------------------------------------------------- /sphinx/notes-ja.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Notes |ja| 3 | ========== 4 | 5 | ------------------------- 6 | AsyncKivyにおける入出力 7 | ------------------------- 8 | 9 | ``asynckivy`` はtrioやasyncioのような入出力機能を持たないのでGUIを固まらせずにそれをしたければ別のスレッドで行うのが良いと思います。 10 | 今のところ関数を別のスレッドで実行する方法には次の二つの方法があります。 11 | 12 | .. code-block:: 13 | 14 | from concurrent.futures import ThreadPoolExecutor 15 | import asynckivy as ak 16 | 17 | executor = ThreadPoolExecutor() 18 | 19 | async def async_func(): 20 | # 方法その一 21 | # 新しくthreadを作ってそこで渡された関数を実行し、その完了を待つ 22 | r = await ak.run_in_thread(外部のスレッドで実行させたい関数) 23 | print("return value:", r) 24 | 25 | # 方法そのニ 26 | # ThreadPoolExecutor内で渡された関数を実行し、その完了を待つ 27 | r = await ak.run_in_executor(executor, 外部のスレッドで実行させたい関数) 28 | print("return value:", r) 29 | 30 | スレッド内で起きた例外(ExceptionではないBaseExceptionは除く)は呼び出し元に運ばれるので、 31 | 以下のように通常の同期コードを書く感覚で例外を捌けます。 32 | 33 | .. code-block:: 34 | 35 | import requests 36 | import asynckivy as ak 37 | 38 | async def async_func(label): 39 | try: 40 | response = await ak.run_in_thread(lambda: requests.get('htt...', timeout=10)) 41 | except requests.Timeout: 42 | label.text = "制限時間内に応答無し" 43 | else: 44 | label.text = "応答有り: " + response.text 45 | 46 | ---------------------------------- 47 | Asyncジェネレータが抱える問題 48 | ---------------------------------- 49 | 50 | ``asyncio`` や ``trio`` がasyncジェネレータに対して `付け焼き刃的な処置 `__ 51 | を行うせいなのか、asynckivy用のasyncジェネレータがうまく機能しない事があります。 52 | なので ``asyncio`` 或いは ``trio`` を使っている場合は以下のAPI達を使わなのがお薦めです。 53 | 54 | * `rest_of_touch_events()` 55 | * `anim_with_xxx` 56 | * `interpolate` 57 | * `interpolate_seq` 58 | * `fade_transition()` 59 | 60 | これにどう対処すればいいのかは現状分かっていません。 61 | もしかすると :pep:`533` が解決してくれるかも。 62 | 63 | ----------------------------- 64 | async操作が禁じられている場所 65 | ----------------------------- 66 | 67 | ``asynckivy`` のAPIでasyncイテレータを返す物のほとんどはその繰り返し中にasync操作を行うことを認めていません。 68 | 以下が該当する者たちです。 69 | 70 | * :func:`asynckivy.rest_of_touch_events` 71 | * :func:`asynckivy.interpolate` 72 | * :func:`asynckivy.interpolate_seq` 73 | * ``asynckivy.anim_with_xxx`` 74 | * :any:`asynckivy.event_freq` 75 | 76 | .. code-block:: 77 | 78 | async for __ in rest_of_touch_events(...): 79 | await awaitable # 駄目 80 | async with async_context_manager: # 駄目 81 | ... 82 | async for __ in async_iterator: # 駄目 83 | ... 84 | -------------------------------------------------------------------------------- /sphinx/notes.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Notes 3 | ===== 4 | 5 | .. _io-in-asynckivy: 6 | 7 | ---------------- 8 | I/O in AsyncKivy 9 | ---------------- 10 | 11 | ``asynckivy`` does not have any I/O primitives unlike ``trio`` and ``asyncio`` do, 12 | thus threads may be the best way to perform them without blocking the main-thread: 13 | 14 | .. code-block:: 15 | 16 | from concurrent.futures import ThreadPoolExecutor 17 | import asynckivy as ak 18 | 19 | executor = ThreadPoolExecutor() 20 | 21 | 22 | def thread_blocking_operation(): 23 | ''' 24 | This function is called from outside the main-thread so you should not 25 | perform any graphics-related operations here. 26 | ''' 27 | 28 | 29 | async def async_fn(): 30 | r = await ak.run_in_thread(thread_blocking_operation) 31 | print("return value:", r) 32 | 33 | r = await ak.run_in_executor(executor, thread_blocking_operation) 34 | print("return value:", r) 35 | 36 | Unhandled exceptions (not :exc:`BaseException`) are propagated to the caller so you can catch them like you do in 37 | synchronous code: 38 | 39 | .. code-block:: 40 | 41 | import requests 42 | import asynckivy as ak 43 | 44 | async def async_fn(label): 45 | try: 46 | response = await ak.run_in_thread(lambda: requests.get('htt...', timeout=10)) 47 | except requests.Timeout: 48 | label.text = "TIMEOUT!" 49 | else: 50 | label.text = "RECEIVED" 51 | 52 | .. _kivys-event-system: 53 | 54 | ------------------- 55 | Kivy's Event System 56 | ------------------- 57 | 58 | (under construction) 59 | 60 | 61 | .. The stop_dispatching can be used to prevent the execution of callbacks (and the default handler) bound to 62 | .. the event. 63 | .. (Though not the all callbacks, but the ones that are bound to the event **before** the call to :func:`event`.) 64 | 65 | .. .. code-block:: 66 | 67 | .. button.bind(on_press=lambda __: print("callback 1")) 68 | .. button.bind(on_press=lambda __: print("callback 2")) 69 | 70 | .. # Wait for a button to be pressed. When that happend, the above callbacks won't be called because they were 71 | .. # bound before the execution of ``await event(...)``. 72 | .. await event(button, 'on_press', stop_dispatching=True) 73 | 74 | .. You may feel weired 75 | 76 | .. .. code-block:: 77 | 78 | .. # Wait for an ``on_touch_down`` event to occur inside a widget. When that happend, the event 79 | .. await event( 80 | .. widget, 'on_touch_down', stop_dispatching=True, 81 | .. filter=lambda w, t: w.collide_point(*t.opos), 82 | .. ) 83 | 84 | .. _the-problem-with-async-generators: 85 | 86 | --------------------------------- 87 | The Problem with Async Generators 88 | --------------------------------- 89 | 90 | :mod:`asyncio` and :mod:`trio` do some hacky stuff, :func:`sys.set_asyncgen_hooks` and :func:`sys.get_asyncgen_hooks`, 91 | which likely hinders asynckivy-flavored async generators. 92 | You can see its details `here `__. 93 | 94 | Because of that, the APIs that create async generators might not work perfectly if ``asyncio`` or ``trio`` is running. 95 | Here is a list of them: 96 | 97 | - :func:`asynckivy.rest_of_touch_events` 98 | - :func:`asynckivy.interpolate` 99 | - :func:`asynckivy.interpolate_seq` 100 | - :func:`asynckivy.fade_transition` 101 | - ``asynckivy.anim_with_xxx`` 102 | 103 | 104 | -------------------------------------------- 105 | Places where async operations are disallowed 106 | -------------------------------------------- 107 | 108 | Most asynckivy APIs that return an async iterator don't allow async operations during iteration. 109 | Here is a list of them: 110 | 111 | - :func:`asynckivy.rest_of_touch_events` 112 | - :func:`asynckivy.interpolate` 113 | - :func:`asynckivy.interpolate_seq` 114 | - ``asynckivy.anim_with_xxx`` 115 | - :any:`asynckivy.event_freq` 116 | 117 | .. code-block:: 118 | 119 | async for __ in rest_of_touch_events(...): 120 | await awaitable # NOT ALLOWED 121 | async with async_context_manager: # NOT ALLOWED 122 | ... 123 | async for __ in async_iterator: # NOT ALLOWED 124 | ... 125 | -------------------------------------------------------------------------------- /sphinx/print_asynckivy_components.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | from importlib import import_module 3 | from importlib.resources.abc import Traversable 4 | from importlib.resources import Package, files 5 | from sphinx.ext.autodoc.mock import mock 6 | 7 | 8 | def enum_private_submodules(package: Package) -> T.Iterator[Traversable]: 9 | for c in files(package).iterdir(): 10 | name = c.name 11 | if name.endswith(".py") and name.startswith("_") and (not name.startswith("__")): 12 | yield c 13 | 14 | 15 | def enum_components(module_name: str) -> T.Iterator[str]: 16 | return iter(import_module(module_name).__all__) 17 | 18 | 19 | def enum_asynckivy_components() -> T.Iterator[str]: 20 | with mock(["kivy", ]): 21 | for submod in enum_private_submodules("asynckivy"): 22 | fullname = "asynckivy." + submod.name[:-3] 23 | yield from enum_components(fullname) 24 | 25 | 26 | def main(): 27 | names = sorted(enum_asynckivy_components()) 28 | print(''.join(( 29 | "__all__ = (\n '", 30 | "',\n '".join(names), 31 | "',\n)\n", 32 | ))) 33 | 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /sphinx/reference.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | API Reference 3 | ============= 4 | 5 | 6 | .. automodule:: asynckivy 7 | :members: 8 | :undoc-members: 9 | :exclude-members: repeat_sleeping 10 | -------------------------------------------------------------------------------- /src/asynckivy/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | 'MotionEventAlreadyEndedError', 3 | 'anim_attrs', 4 | 'anim_attrs_abbr', 5 | 'anim_with_dt', 6 | 'anim_with_dt_et', 7 | 'anim_with_dt_et_ratio', 8 | 'anim_with_et', 9 | 'anim_with_ratio', 10 | 'event', 11 | 'event_freq', 12 | 'fade_transition', 13 | 'interpolate', 14 | 'interpolate_seq', 15 | 'managed_start', 16 | 'move_on_after', 17 | 'n_frames', 18 | 'repeat_sleeping', 19 | 'rest_of_touch_events', 20 | 'run_in_executor', 21 | 'run_in_thread', 22 | 'sleep', 23 | 'sleep_free', 24 | 'smooth_attr', 25 | 'suppress_event', 26 | 'sync_attr', 27 | 'sync_attrs', 28 | 'transform', 29 | ) 30 | 31 | from asyncgui import * 32 | from ._exceptions import MotionEventAlreadyEndedError 33 | from ._sleep import sleep, sleep_free, repeat_sleeping, move_on_after, n_frames 34 | from ._event import event, event_freq, suppress_event, rest_of_touch_events 35 | from ._anim_with_xxx import anim_with_dt, anim_with_et, anim_with_ratio, anim_with_dt_et, anim_with_dt_et_ratio 36 | from ._anim_attrs import anim_attrs, anim_attrs_abbr 37 | from ._interpolate import interpolate, interpolate_seq, fade_transition 38 | from ._threading import run_in_executor, run_in_thread 39 | from ._etc import transform, sync_attr, sync_attrs 40 | from ._managed_start import managed_start 41 | from ._smooth_attr import smooth_attr 42 | -------------------------------------------------------------------------------- /src/asynckivy/_anim_attrs.py: -------------------------------------------------------------------------------- 1 | __all__ = ('anim_attrs', 'anim_attrs_abbr', ) 2 | import typing as T 3 | import types 4 | from functools import partial 5 | import kivy.clock 6 | from kivy.animation import AnimationTransition 7 | import asyncgui 8 | 9 | 10 | def _update(setattr, zip, min, obj, duration, transition, output_seq_type, anim_params, task, p_time, dt): 11 | time = p_time[0] + dt 12 | p_time[0] = time 13 | 14 | # calculate progression 15 | progress = min(1., time / duration) 16 | t = transition(progress) 17 | 18 | # apply progression on obj 19 | for attr_name, org_value, slope, is_seq in anim_params: 20 | if is_seq: 21 | new_value = output_seq_type( 22 | slope_elem * t + org_elem 23 | for org_elem, slope_elem in zip(org_value, slope) 24 | ) 25 | setattr(obj, attr_name, new_value) 26 | else: 27 | setattr(obj, attr_name, slope * t + org_value) 28 | 29 | # time to stop ? 30 | if progress >= 1.: 31 | task._step() 32 | return False 33 | 34 | 35 | _update = partial(_update, setattr, zip, min) 36 | 37 | 38 | @types.coroutine 39 | def _anim_attrs( 40 | obj, duration, step, transition, output_seq_type, animated_properties, 41 | getattr=getattr, isinstance=isinstance, tuple=tuple, str=str, partial=partial, native_seq_types=(tuple, list), 42 | zip=zip, Clock=kivy.clock.Clock, AnimationTransition=AnimationTransition, 43 | _update=_update, _current_task=asyncgui._current_task, _sleep_forever=asyncgui._sleep_forever, /): 44 | if isinstance(transition, str): 45 | transition = getattr(AnimationTransition, transition) 46 | 47 | # get current values & calculate slopes 48 | anim_params = tuple( 49 | ( 50 | org_value := getattr(obj, attr_name), 51 | is_seq := isinstance(org_value, native_seq_types), 52 | ( 53 | org_value := tuple(org_value), 54 | slope := tuple(goal_elem - org_elem for goal_elem, org_elem in zip(goal_value, org_value)), 55 | ) if is_seq else (slope := goal_value - org_value), 56 | ) and (attr_name, org_value, slope, is_seq, ) 57 | for attr_name, goal_value in animated_properties.items() 58 | ) 59 | 60 | try: 61 | clock_event = Clock.schedule_interval( 62 | partial(_update, obj, duration, transition, output_seq_type, anim_params, (yield _current_task)[0][0], 63 | [0., ]), 64 | step, 65 | ) 66 | yield _sleep_forever 67 | finally: 68 | clock_event.cancel() 69 | 70 | 71 | def anim_attrs(obj, *, duration=1.0, step=0, transition=AnimationTransition.linear, output_seq_type=tuple, 72 | **animated_properties) -> T.Awaitable: 73 | ''' 74 | Animates attibutes of any object. 75 | 76 | .. code-block:: 77 | 78 | import types 79 | 80 | obj = types.SimpleNamespace(x=0, size=(200, 300)) 81 | await anim_attrs(obj, x=100, size=(400, 400)) 82 | 83 | The ``output_seq_type`` parameter: 84 | 85 | .. code-block:: 86 | 87 | obj = types.SimpleNamespace(size=(200, 300)) 88 | await anim_attrs(obj, size=(400, 400), output_seq_type=list) 89 | assert type(obj.size) is list 90 | 91 | .. warning:: 92 | 93 | Unlike :class:`kivy.animation.Animation`, this one does not support dictionary-type and nested-sequence. 94 | 95 | .. code-block:: 96 | 97 | await anim_attrs(obj, pos_hint={'x': 1.}) # not supported 98 | await anim_attrs(obj, nested_sequence=[[10, 20, ]]) # not supported 99 | 100 | await anim_attrs(obj, color=(1, 0, 0, 1), pos=(100, 200)) # OK 101 | 102 | .. versionadded:: 0.6.1 103 | ''' 104 | return _anim_attrs(obj, duration, step, transition, output_seq_type, animated_properties) 105 | 106 | 107 | def anim_attrs_abbr(obj, *, d=1.0, s=0, t=AnimationTransition.linear, output_seq_type=tuple, 108 | **animated_properties) -> T.Awaitable: 109 | ''' 110 | :func:`anim_attrs` cannot animate attributes named ``step``, ``duration`` and ``transition`` but this one can. 111 | 112 | .. versionadded:: 0.6.1 113 | ''' 114 | return _anim_attrs(obj, d, s, t, output_seq_type, animated_properties) 115 | -------------------------------------------------------------------------------- /src/asynckivy/_anim_with_xxx.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | 'anim_with_dt', 'anim_with_et', 'anim_with_ratio', 'anim_with_dt_et', 'anim_with_dt_et_ratio', 3 | ) 4 | 5 | from ._sleep import repeat_sleeping 6 | 7 | 8 | async def anim_with_dt(*, step=0): 9 | ''' 10 | An async form of :meth:`kivy.clock.Clock.schedule_interval`. The following callback-style code: 11 | 12 | .. code-block:: 13 | 14 | def callback(dt): 15 | print(dt) 16 | if some_condition: 17 | return False 18 | 19 | Clock.schedule_interval(callback, 0.1) 20 | 21 | is equivalent to the following async-style code: 22 | 23 | .. code-block:: 24 | 25 | async for dt in anim_with_dt(step=0.1): 26 | print(dt) 27 | if some_condition: 28 | break 29 | 30 | .. versionadded:: 0.6.1 31 | ''' 32 | async with repeat_sleeping(step=step) as sleep: 33 | while True: 34 | yield await sleep() 35 | 36 | 37 | async def anim_with_et(*, step=0): 38 | ''' 39 | Returns an async iterator that yields the elapsed time since the start of the iteration. 40 | 41 | .. code-block:: 42 | 43 | async for et in anim_with_et(...): 44 | print(et) 45 | 46 | The code above is equivalent to the following: 47 | 48 | .. code-block:: 49 | 50 | et = 0. 51 | async for dt in anim_with_dt(...): 52 | et += dt 53 | print(et) 54 | 55 | .. versionadded:: 0.6.1 56 | ''' 57 | et = 0. 58 | async with repeat_sleeping(step=step) as sleep: 59 | while True: 60 | et += await sleep() 61 | yield et 62 | 63 | 64 | async def anim_with_dt_et(*, step=0): 65 | ''' 66 | :func:`anim_with_dt` and :func:`anim_with_et` combined. 67 | 68 | .. code-block:: 69 | 70 | async for dt, et in anim_with_dt_et(...): 71 | ... 72 | 73 | .. versionadded:: 0.6.1 74 | ''' 75 | et = 0. 76 | async with repeat_sleeping(step=step) as sleep: 77 | while True: 78 | dt = await sleep() 79 | et += dt 80 | yield dt, et 81 | 82 | 83 | async def anim_with_ratio(*, base, step=0): 84 | ''' 85 | Returns an async iterator that yields the elapsed time since the start of the iteration, divided by ``base``. 86 | 87 | .. code-block:: 88 | 89 | async for p in anim_with_ratio(base=3): 90 | print(p) 91 | 92 | The code above is equivalent to the following: 93 | 94 | .. code-block:: 95 | 96 | base = 3 97 | async for et in anim_with_et(): 98 | p = et / base 99 | print(p) 100 | 101 | If you want non-linear ratio values, you may find :class:`kivy.animation.AnimationTransition` helpful. 102 | 103 | .. code-block:: 104 | 105 | from kivy.animation import AnimationTransition 106 | 107 | in_cubic = AnimationTransition.in_cubic 108 | 109 | async for p in anim_with_ratio(base=...): 110 | p = in_cubic(p) 111 | print(p) 112 | 113 | .. versionadded:: 0.6.1 114 | 115 | .. versionchanged:: 0.7.0 116 | 117 | The ``duration`` parameter was replaced with ``base``. 118 | The loop no longer ends on its own. 119 | ''' 120 | async with repeat_sleeping(step=step) as sleep: 121 | et = 0. 122 | while True: 123 | et += await sleep() 124 | yield et / base 125 | 126 | 127 | async def anim_with_dt_et_ratio(*, base, step=0): 128 | ''' 129 | :func:`anim_with_dt`, :func:`anim_with_et` and :func:`anim_with_ratio` combined. 130 | 131 | .. code-block:: 132 | 133 | async for dt, et, p in anim_with_dt_et_ratio(...): 134 | ... 135 | 136 | .. versionadded:: 0.6.1 137 | 138 | .. versionchanged:: 0.7.0 139 | The ``duration`` parameter was replaced with ``base``. 140 | The loop no longer ends on its own. 141 | ''' 142 | async with repeat_sleeping(step=step) as sleep: 143 | et = 0. 144 | while True: 145 | dt = await sleep() 146 | et += dt 147 | yield dt, et, et / base 148 | -------------------------------------------------------------------------------- /src/asynckivy/_etc.py: -------------------------------------------------------------------------------- 1 | __all__ = ("transform", "sync_attr", "sync_attrs", ) 2 | import typing as T 3 | from contextlib import contextmanager 4 | from functools import partial 5 | 6 | from kivy.event import EventDispatcher 7 | from kivy.graphics import PushMatrix, PopMatrix, InstructionGroup 8 | 9 | 10 | @contextmanager 11 | def transform(widget, *, use_outer_canvas=False) -> T.ContextManager[InstructionGroup]: 12 | ''' 13 | Returns a context manager that sandwiches the ``widget``'s existing canvas instructions between 14 | a :class:`kivy.graphics.PushMatrix` and a :class:`kivy.graphics.PopMatrix`, and inserts an 15 | :class:`kivy.graphics.InstructionGroup` right next to the ``PushMatrix``. Those three instructions will be removed 16 | when the context manager exits. 17 | 18 | This may be useful when you want to animate a widget. 19 | 20 | **Usage** 21 | 22 | .. code-block:: 23 | 24 | from kivy.graphics import Rotate 25 | 26 | async def rotate_widget(widget, *, angle=360.): 27 | with transform(widget) as ig: # <- InstructionGroup 28 | ig.add(rotate := Rotate(origin=widget.center)) 29 | await anim_attrs(rotate, angle=angle) 30 | 31 | If the position or size of the ``widget`` changes during the animation, you might need :class:`sync_attr`. 32 | 33 | **The use_outer_canvas parameter** 34 | 35 | While the context manager is active, the content of the widget's canvas would be: 36 | 37 | .. code-block:: yaml 38 | 39 | # ... represents existing instructions 40 | 41 | Widget: 42 | canvas.before: 43 | ... 44 | canvas: 45 | PushMatrix 46 | InstructionGroup 47 | ... 48 | PopMatrix 49 | canvas.after: 50 | ... 51 | 52 | but if ``use_outer_canvas`` is True, it would be: 53 | 54 | .. code-block:: yaml 55 | 56 | Widget: 57 | canvas.before: 58 | PushMatrix 59 | InstructionGroup 60 | ... 61 | canvas: 62 | ... 63 | canvas.after: 64 | ... 65 | PopMatrix 66 | ''' 67 | 68 | c = widget.canvas 69 | if use_outer_canvas: 70 | before = c.before 71 | after = c.after 72 | push_mat_idx = 0 73 | ig_idx = 1 74 | else: 75 | c.before # ensure 'canvas.before' exists 76 | 77 | # Index starts from 1 because 'canvas.before' is sitting at index 0 and we usually want it to remain first. 78 | # See https://github.com/kivy/kivy/issues/7945 for details. 79 | push_mat_idx = 1 80 | ig_idx = 2 81 | before = after = c 82 | 83 | push_mat = PushMatrix() 84 | ig = InstructionGroup() 85 | pop_mat = PopMatrix() 86 | 87 | before.insert(push_mat_idx, push_mat) 88 | before.insert(ig_idx, ig) 89 | after.add(pop_mat) 90 | try: 91 | yield ig 92 | finally: 93 | after.remove(pop_mat) 94 | before.remove(ig) 95 | before.remove(push_mat) 96 | 97 | 98 | class sync_attr: 99 | ''' 100 | Creates one-directional binding between attributes. 101 | 102 | .. code-block:: 103 | 104 | import types 105 | 106 | widget = Widget(x=100) 107 | obj = types.SimpleNamespace() 108 | 109 | sync_attr(from_=(widget, 'x'), to_=(obj, 'xx')) 110 | assert obj.xx == 100 # synchronized 111 | widget.x = 10 112 | assert obj.xx == 10 # synchronized 113 | obj.xx = 20 114 | assert widget.x == 10 # but not the other way around 115 | 116 | To make its effect temporary, use it with a with-statement: 117 | 118 | .. code-block:: 119 | 120 | # The effect lasts only within the with-block. 121 | with sync_attr(...): 122 | ... 123 | 124 | This can be particularly useful when combined with :func:`transform`. 125 | 126 | .. code-block:: 127 | 128 | from kivy.graphics import Rotate 129 | 130 | async def rotate_widget(widget, *, angle=360.): 131 | rotate = Rotate() 132 | with ( 133 | transform(widget) as ig, 134 | sync_attr(from_=(widget, 'center'), to_=(rotate, 'origin')), 135 | ): 136 | ig.add(rotate) 137 | await anim_attrs(rotate, angle=angle) 138 | 139 | .. versionadded:: 0.6.1 140 | 141 | .. versionchanged:: 0.8.0 142 | The context manager now applies its effect upon creation, rather than when its ``__enter__()`` method is 143 | called, and ``__enter__()`` no longer performs any action. 144 | 145 | Additionally, the context manager now assigns the ``from_`` value to the ``to_`` upon creation: 146 | 147 | .. code-block:: 148 | 149 | with sync_attr((widget, 'x'), (obj, 'xx')): 150 | assert widget.x == obj.xx 151 | ''' 152 | __slots__ = ("__exit__", ) 153 | 154 | def __init__(self, from_: tuple[EventDispatcher, str], to_: tuple[T.Any, str]): 155 | setattr(*to_, getattr(*from_)) 156 | bind_uid = from_[0].fbind(from_[1], partial(self._sync, setattr, *to_)) 157 | self.__exit__ = partial(self._unbind, *from_, bind_uid) 158 | 159 | @staticmethod 160 | def _sync(setattr, obj, attr_name, event_dispatcher, new_value): 161 | setattr(obj, attr_name, new_value) 162 | 163 | @staticmethod 164 | def _unbind(event_dispatcher, event_name, bind_uid, *__): 165 | event_dispatcher.unbind_uid(event_name, bind_uid) 166 | 167 | def __enter__(self): 168 | pass 169 | 170 | 171 | class sync_attrs: 172 | ''' 173 | When multiple :class:`sync_attr` calls take the same ``from_`` argument, they can be merged into a single 174 | :class:`sync_attrs` call. For instance, the following code: 175 | 176 | .. code-block:: 177 | 178 | with sync_attr((widget, 'x'), (obj1, 'x')), sync_attr((widget, 'x'), (obj2, 'xx')): 179 | ... 180 | 181 | can be replaced with the following one: 182 | 183 | .. code-block:: 184 | 185 | with sync_attrs((widget, 'x'), (obj1, 'x'), (obj2, 'xx')): 186 | ... 187 | 188 | This can be particularly useful when combined with :func:`transform`. 189 | 190 | .. code-block:: 191 | 192 | from kivy.graphics import Rotate, Scale 193 | 194 | async def scale_and_rotate_widget(widget, *, scale=2.0, angle=360.): 195 | rotate = Rotate() 196 | scale = Scale() 197 | with ( 198 | transform(widget) as ig, 199 | sync_attrs((widget, 'center'), (rotate, 'origin'), (scale, 'origin')), 200 | ): 201 | ig.add(rotate) 202 | ig.add(scale) 203 | await wait_all( 204 | anim_attrs(rotate, angle=angle), 205 | anim_attrs(scale, x=scale, y=scale), 206 | ) 207 | 208 | .. versionadded:: 0.6.1 209 | 210 | .. versionchanged:: 0.8.0 211 | The context manager now applies its effect upon creation, rather than when its ``__enter__()`` method is 212 | called, and ``__enter__()`` no longer performs any action. 213 | 214 | Additionally, the context manager now assigns the ``from_`` value to the ``to_`` upon creation: 215 | 216 | .. code-block:: 217 | 218 | with sync_attrs((widget, 'x'), (obj, 'xx')): 219 | assert widget.x is obj.xx 220 | ''' 221 | __slots__ = ("__exit__", ) 222 | 223 | def __init__(self, from_: tuple[EventDispatcher, str], *to_): 224 | sync = partial(self._sync, setattr, to_) 225 | sync(None, getattr(*from_)) 226 | bind_uid = from_[0].fbind(from_[1], sync) 227 | self.__exit__ = partial(self._unbind, *from_, bind_uid) 228 | 229 | @staticmethod 230 | def _sync(setattr, to_, event_dispatcher, new_value): 231 | for obj, attr_name in to_: 232 | setattr(obj, attr_name, new_value) 233 | 234 | _unbind = staticmethod(sync_attr._unbind) 235 | 236 | def __enter__(self): 237 | pass 238 | -------------------------------------------------------------------------------- /src/asynckivy/_event.py: -------------------------------------------------------------------------------- 1 | __all__ = ("event", "event_freq", "suppress_event", "rest_of_touch_events", ) 2 | 3 | import typing as T 4 | import types 5 | from functools import partial 6 | from contextlib import nullcontext 7 | 8 | from asyncgui import _current_task, _sleep_forever, move_on_when, wait_any 9 | 10 | from ._exceptions import MotionEventAlreadyEndedError 11 | from ._sleep import sleep 12 | 13 | 14 | @types.coroutine 15 | def event(event_dispatcher, event_name, *, filter=None, stop_dispatching=False) -> T.Awaitable[tuple]: 16 | ''' 17 | Returns an awaitable that can be used to wait for: 18 | 19 | * a Kivy event to occur. 20 | * a Kivy property's value to change. 21 | 22 | .. code-block:: 23 | 24 | # Wait for a button to be pressed. 25 | await event(button, 'on_press') 26 | 27 | # Wait for an 'on_touch_down' event to occur. 28 | __, touch = await event(widget, 'on_touch_down') 29 | 30 | # Wait for 'widget.x' to change. 31 | __, x = await ak.event(widget, 'x') 32 | 33 | The ``filter`` parameter: 34 | 35 | .. code-block:: 36 | 37 | # Wait for an 'on_touch_down' event to occur inside a widget. 38 | __, touch = await event(widget, 'on_touch_down', filter=lambda w, t: w.collide_point(*t.opos)) 39 | 40 | # Wait for 'widget.x' to become greater than 100. 41 | if widget.x <= 100: 42 | await event(widget, 'x', filter=lambda __, x: x > 100) 43 | 44 | The ``stop_dispatching`` parameter: 45 | 46 | It only works for events not for properties. 47 | See :ref:`kivys-event-system` for details. 48 | ''' 49 | task = (yield _current_task)[0][0] 50 | bind_id = event_dispatcher.fbind(event_name, partial(_event_callback, filter, task._step, stop_dispatching)) 51 | assert bind_id # check if binding succeeded 52 | try: 53 | return (yield _sleep_forever)[0] 54 | finally: 55 | event_dispatcher.unbind_uid(event_name, bind_id) 56 | 57 | 58 | def _event_callback(filter, task_step, stop_dispatching, *args, **kwargs): 59 | if (filter is None) or filter(*args, **kwargs): 60 | task_step(*args) 61 | return stop_dispatching 62 | 63 | 64 | class event_freq: 65 | ''' 66 | When handling a frequently occurring event, such as ``on_touch_move``, the following code might cause performance 67 | issues: 68 | 69 | .. code-block:: 70 | 71 | __, touch = await event(widget, 'on_touch_down') 72 | while True: 73 | await event(widget, 'on_touch_move', filter=lambda w, t: t is touch) 74 | ... 75 | 76 | If that happens, try the following code instead. It might resolve the issue: 77 | 78 | .. code-block:: 79 | 80 | __, touch = await event(widget, 'on_touch_down') 81 | async with event_freq(widget, 'on_touch_move', filter=lambda w, t: t is touch) as on_touch_move: 82 | while True: 83 | await on_touch_move() 84 | ... 85 | 86 | The trade-off is that within the context manager, you can't perform any async operations except the 87 | ``await on_touch_move()``. 88 | 89 | .. code-block:: 90 | 91 | async with event_freq(...) as xxx: 92 | await xxx() # OK 93 | await something_else() # Don't 94 | 95 | .. versionadded:: 0.7.1 96 | ''' 97 | __slots__ = ('_disp', '_name', '_filter', '_stop', '_bind_id', ) 98 | 99 | def __init__(self, event_dispatcher, event_name, *, filter=None, stop_dispatching=False): 100 | self._disp = event_dispatcher 101 | self._name = event_name 102 | self._filter = filter 103 | self._stop = stop_dispatching 104 | 105 | @types.coroutine 106 | def __aenter__(self): 107 | task = (yield _current_task)[0][0] 108 | self._bind_id = self._disp.fbind(self._name, partial(_event_callback, self._filter, task._step, self._stop)) 109 | return self._wait_one 110 | 111 | async def __aexit__(self, *args): 112 | self._disp.unbind_uid(self._name, self._bind_id) 113 | 114 | @staticmethod 115 | @types.coroutine 116 | def _wait_one(): 117 | return (yield _sleep_forever)[0] 118 | 119 | 120 | class suppress_event: 121 | ''' 122 | Returns a context manager that prevents the callback functions (including the default handler) bound to an event 123 | from being called. 124 | 125 | .. code-block:: 126 | :emphasize-lines: 4 127 | 128 | from kivy.uix.button import Button 129 | 130 | btn = Button() 131 | btn.bind(on_press=lambda __: print("pressed")) 132 | with suppress_event(btn, 'on_press'): 133 | btn.dispatch('on_press') 134 | 135 | The above code prints nothing because the callback function won't be called. 136 | 137 | Strictly speaking, this context manager doesn't prevent all callback functions from being called. 138 | It only prevents the callback functions that were bound to an event before the context manager enters. 139 | Thus, the following code prints ``pressed``. 140 | 141 | .. code-block:: 142 | :emphasize-lines: 5 143 | 144 | from kivy.uix.button import Button 145 | 146 | btn = Button() 147 | with suppress_event(btn, 'on_press'): 148 | btn.bind(on_press=lambda __: print("pressed")) 149 | btn.dispatch('on_press') 150 | 151 | .. warning:: 152 | 153 | You need to be careful when you suppress an ``on_touch_xxx`` event. 154 | See :ref:`kivys-event-system` for details. 155 | ''' 156 | __slots__ = ('_dispatcher', '_name', '_bind_uid', '_filter', ) 157 | 158 | def __init__(self, event_dispatcher, event_name, *, filter=lambda *args, **kwargs: True): 159 | self._dispatcher = event_dispatcher 160 | self._name = event_name 161 | self._filter = filter 162 | 163 | def __enter__(self): 164 | self._bind_uid = self._dispatcher.fbind(self._name, self._filter) 165 | 166 | def __exit__(self, *args): 167 | self._dispatcher.unbind_uid(self._name, self._bind_uid) 168 | 169 | 170 | async def rest_of_touch_events(widget, touch, *, stop_dispatching=False, timeout=1.) -> T.AsyncIterator[None]: 171 | ''' 172 | Returns an async iterator that yields None on each ``on_touch_move`` event 173 | and stops when an ``on_touch_up`` event occurs. 174 | 175 | .. code-block:: 176 | 177 | async for __ in rest_of_touch_events(widget, touch): 178 | print('on_touch_move') 179 | print('on_touch_up') 180 | 181 | **Caution** 182 | 183 | * If the ``widget`` is the type of widget that grabs touches by itself, such as :class:`kivy.uix.button.Button`, 184 | you probably want to set the ``stop_dispatching`` parameter to True in most cases. 185 | * There are widgets/behaviors that might simulate touch events (e.g. :class:`kivy.uix.scrollview.ScrollView`, 186 | :class:`kivy.uix.behaviors.DragBehavior` and ``kivy_garden.draggable.KXDraggableBehavior``). 187 | If many such widgets are in the parent stack of the ``widget``, this API might mistakenly raise a 188 | :exc:`asynckivy.MotionEventAlreadyEndedError`. If that happens, increase the ``timeout`` parameter. 189 | ''' 190 | if touch.time_end != -1: 191 | # An `on_touch_up`` event might have already been fired, so we need to determine 192 | # whether it actually was or not. 193 | tasks = await wait_any( 194 | sleep(timeout), 195 | event(widget, 'on_touch_up', filter=lambda w, t: t is touch), 196 | ) 197 | if tasks[0].finished: 198 | raise MotionEventAlreadyEndedError(f"MotionEvent(uid={touch.uid}) has already ended") 199 | return 200 | try: 201 | touch.grab(widget) 202 | if stop_dispatching: 203 | se = partial(suppress_event, widget, filter=lambda w, t, touch=touch: t is touch) 204 | with ( 205 | se("on_touch_up") if stop_dispatching else nullcontext(), 206 | se("on_touch_move") if stop_dispatching else nullcontext(), 207 | ): 208 | def filter(w, t, touch=touch): 209 | return t is touch and t.grab_current is w 210 | async with ( 211 | move_on_when(event(widget, "on_touch_up", filter=filter, stop_dispatching=True)), 212 | event_freq(widget, 'on_touch_move', filter=filter, stop_dispatching=True) as on_touch_move, 213 | ): 214 | while True: 215 | await on_touch_move() 216 | yield 217 | finally: 218 | touch.ungrab(widget) 219 | -------------------------------------------------------------------------------- /src/asynckivy/_exceptions.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | 'MotionEventAlreadyEndedError', 3 | ) 4 | 5 | 6 | class MotionEventAlreadyEndedError(Exception): 7 | ''' 8 | This error occurs when an already-ended touch is passed to an asynckivy API that expects an ongoing touch. 9 | For instance: 10 | 11 | .. code-block:: 12 | :emphasize-lines: 4 13 | 14 | import asynckivy as ak 15 | 16 | class MyWidget(Widget): 17 | def on_touch_up(self, touch): # not 'on_touch_down', oops! 18 | ak.start(self.handle_touch(touch)) 19 | return True 20 | 21 | async def handle_touch(self, touch): 22 | try: 23 | async for __ in ak.rest_of_touch_events(widget, touch): 24 | ... 25 | except ak.MotionEventAlreadyEndedError: 26 | ... 27 | ''' 28 | -------------------------------------------------------------------------------- /src/asynckivy/_interpolate.py: -------------------------------------------------------------------------------- 1 | __all__ = ('interpolate', 'interpolate_seq', 'fade_transition', ) 2 | import typing as T 3 | from contextlib import asynccontextmanager 4 | from kivy.animation import AnimationTransition 5 | 6 | from ._sleep import sleep 7 | from ._anim_with_xxx import anim_with_ratio 8 | 9 | 10 | linear = AnimationTransition.linear 11 | 12 | 13 | async def interpolate(start, end, *, duration=1.0, step=0, transition=linear) -> T.AsyncIterator: 14 | ''' 15 | Interpolates between the values ``start`` and ``end`` in an async-manner. 16 | Inspired by wasabi2d's interpolate_. 17 | 18 | .. code-block:: 19 | 20 | async for v in interpolate(0, 100, duration=1.0, step=.3): 21 | print(int(v)) 22 | 23 | ============ ====== 24 | elapsed time output 25 | ============ ====== 26 | 0 sec 0 27 | 0.3 sec 30 28 | 0.6 sec 60 29 | 0.9 sec 90 30 | **1.2 sec** 100 31 | ============ ====== 32 | 33 | .. _interpolate: https://wasabi2d.readthedocs.io/en/stable/coros.html#clock.coro.interpolate 34 | ''' 35 | if isinstance(transition, str): 36 | transition = getattr(AnimationTransition, transition) 37 | 38 | slope = end - start 39 | yield transition(0.) * slope + start 40 | if duration: 41 | async for p in anim_with_ratio(step=step, base=duration): 42 | if p >= 1.0: 43 | break 44 | yield transition(p) * slope + start 45 | else: 46 | await sleep(0) 47 | yield transition(1.) * slope + start 48 | 49 | 50 | async def interpolate_seq(start, end, *, duration, step=0, transition=linear, output_type=tuple) -> T.AsyncIterator: 51 | ''' 52 | Same as :func:`interpolate` except this one is for sequence types. 53 | 54 | .. code-block:: 55 | 56 | async for v in interpolate_seq([0, 50], [100, 100], duration=1, step=0.3): 57 | print(v) 58 | 59 | ============ ========== 60 | elapsed time output 61 | ============ ========== 62 | 0 (0, 50) 63 | 0.3 (30, 65) 64 | 0.6 (60, 80) 65 | 0.9 (90, 95) 66 | **1.2 sec** (100, 100) 67 | ============ ========== 68 | 69 | .. versionadded:: 0.7.0 70 | ''' 71 | if isinstance(transition, str): 72 | transition = getattr(AnimationTransition, transition) 73 | zip_ = zip 74 | slope = tuple(end_elem - start_elem for end_elem, start_elem in zip_(end, start)) 75 | 76 | p = transition(0.) 77 | yield output_type(p * slope_elem + start_elem for slope_elem, start_elem in zip_(slope, start)) 78 | 79 | if duration: 80 | async for p in anim_with_ratio(step=step, base=duration): 81 | if p >= 1.0: 82 | break 83 | p = transition(p) 84 | yield output_type(p * slope_elem + start_elem for slope_elem, start_elem in zip_(slope, start)) 85 | else: 86 | await sleep(0) 87 | 88 | p = transition(1.) 89 | yield output_type(p * slope_elem + start_elem for slope_elem, start_elem in zip_(slope, start)) 90 | 91 | 92 | @asynccontextmanager 93 | async def fade_transition(*widgets, duration=1.0, step=0) -> T.AsyncContextManager: 94 | ''' 95 | Returns an async context manager that: 96 | 97 | * fades-out the given widgets on ``__aenter__``. 98 | * fades-in the given widgets on ``__aexit__``. 99 | 100 | .. code-block:: 101 | 102 | async with fade_transition(widget1, widget2): 103 | ... 104 | 105 | The ``widgets`` don't have to be actual Kivy widgets. 106 | Anything that has an attribute named ``opacity`` would work. 107 | ''' 108 | half_duration = duration / 2. 109 | org_opas = tuple(w.opacity for w in widgets) 110 | try: 111 | async for p in anim_with_ratio(base=half_duration, step=step): 112 | p = 1.0 - p 113 | for w, o in zip(widgets, org_opas): 114 | w.opacity = p * o 115 | if p <= 0.: 116 | break 117 | yield 118 | async for p in anim_with_ratio(base=half_duration, step=step): 119 | for w, o in zip(widgets, org_opas): 120 | w.opacity = p * o 121 | if p >= 1.: 122 | break 123 | finally: 124 | for w, o in zip(widgets, org_opas): 125 | w.opacity = o 126 | -------------------------------------------------------------------------------- /src/asynckivy/_managed_start.py: -------------------------------------------------------------------------------- 1 | __all__ = ("managed_start", ) 2 | 3 | import asyncgui 4 | from kivy.clock import Clock 5 | 6 | 7 | async def _setup(): 8 | global _global_nursery 9 | async with asyncgui.open_nursery() as nursery: 10 | _global_nursery = nursery 11 | await asyncgui.sleep_forever() 12 | 13 | 14 | _global_nursery = None 15 | _root_task = asyncgui.start(_setup()) 16 | managed_start = _global_nursery.start 17 | 18 | __managed_start_doc__ = ''' 19 | A task started with this function will be automatically cancelled when an ``App.on_stop`` 20 | event fires, if it is still running. This prevents the task from being cancelled by the garbage 21 | collector, ensuring more reliable cleanup. You should always use this function instead of calling 22 | ``asynckivy.start`` directly, except when writing unit tests. 23 | 24 | .. code-block:: 25 | 26 | task = managed_start(async_func(...)) 27 | 28 | .. versionadded:: 0.7.1 29 | ''' 30 | 31 | 32 | def _schedule_teardown(dt): 33 | from kivy.app import App 34 | app = App.get_running_app() 35 | if app is None: 36 | return 37 | app.fbind("on_stop", lambda __: _root_task.cancel()) 38 | 39 | 40 | Clock.schedule_once(_schedule_teardown) 41 | -------------------------------------------------------------------------------- /src/asynckivy/_sleep.py: -------------------------------------------------------------------------------- 1 | __all__ = ("sleep", "sleep_free", "repeat_sleeping", "move_on_after", "n_frames", ) 2 | 3 | import typing as T 4 | import types 5 | 6 | from kivy.clock import Clock 7 | from asyncgui import _current_task, _sleep_forever, move_on_when, Task, Cancelled 8 | 9 | 10 | @types.coroutine 11 | def sleep(duration) -> T.Awaitable[float]: 12 | ''' 13 | An async form of :meth:`kivy.clock.Clock.schedule_once`. 14 | 15 | .. code-block:: 16 | 17 | dt = await sleep(5) # wait for 5 seconds 18 | ''' 19 | task = (yield _current_task)[0][0] 20 | clock_event = Clock.create_trigger(task._step, duration, False, False) 21 | clock_event() 22 | 23 | try: 24 | return (yield _sleep_forever)[0][0] 25 | except Cancelled: 26 | clock_event.cancel() 27 | raise 28 | 29 | 30 | @types.coroutine 31 | def sleep_free(duration) -> T.Awaitable[float]: 32 | ''' 33 | An async form of :meth:`kivy.clock.Clock.schedule_once_free`. 34 | 35 | .. code-block:: 36 | 37 | dt = await sleep_free(5) # wait for 5 seconds 38 | ''' 39 | task = (yield _current_task)[0][0] 40 | clock_event = Clock.create_trigger_free(task._step, duration, False, False) 41 | clock_event() 42 | 43 | try: 44 | return (yield _sleep_forever)[0][0] 45 | except Cancelled: 46 | clock_event.cancel() 47 | raise 48 | 49 | 50 | class repeat_sleeping: 51 | ''' 52 | Returns an async context manager that provides an efficient way to repeat sleeping. 53 | 54 | When there is a piece of code like this: 55 | 56 | .. code-block:: 57 | 58 | while True: 59 | await sleep(0) 60 | ... 61 | 62 | it can be translated to: 63 | 64 | .. code-block:: 65 | 66 | async with repeat_sleeping(step=0) as sleep: 67 | while True: 68 | await sleep() 69 | ... 70 | 71 | The latter is more suitable for situations requiring frequent sleeps, such as moving an object in every frame. 72 | 73 | **Restriction** 74 | 75 | You are not allowed to perform any kind of async operations inside the with-block except you can 76 | ``await`` the return value of the function that is bound to the identifier of the as-clause. 77 | 78 | .. code-block:: 79 | 80 | async with repeat_sleeping(step=0) as sleep: 81 | await sleep() # OK 82 | await something_else # NOT ALLOWED 83 | async with async_context_manager: # NOT ALLOWED 84 | ... 85 | async for __ in async_iterator: # NOT ALLOWED 86 | ... 87 | 88 | .. versionchanged:: 0.8.0 89 | 90 | This API is now private. 91 | ''' 92 | 93 | __slots__ = ('_step', '_trigger', ) 94 | 95 | @types.coroutine 96 | def _sleep(_f=_sleep_forever): 97 | return (yield _f)[0][0] 98 | 99 | def __init__(self, *, step=0): 100 | self._step = step 101 | 102 | @types.coroutine 103 | def __aenter__(self, _sleep=_sleep) -> T.Awaitable[T.Callable[[], T.Awaitable[float]]]: 104 | task = (yield _current_task)[0][0] 105 | self._trigger = Clock.create_trigger(task._step, self._step, True, False) 106 | self._trigger() 107 | return _sleep 108 | 109 | async def __aexit__(self, exc_type, exc_val, exc_tb): 110 | self._trigger.cancel() 111 | 112 | 113 | def move_on_after(seconds: float) -> T.AsyncContextManager[Task]: 114 | ''' 115 | Returns an async context manager that applies a time limit to its code block, 116 | like :func:`trio.move_on_after` does. 117 | 118 | .. code-block:: 119 | 120 | async with move_on_after(seconds) as timeout_tracker: 121 | ... 122 | if timeout_tracker.finished: 123 | print("The code block was interrupted due to a timeout") 124 | else: 125 | print("The code block exited gracefully.") 126 | 127 | .. versionadded:: 0.6.1 128 | ''' 129 | return move_on_when(sleep(seconds)) 130 | 131 | 132 | @types.coroutine 133 | def n_frames(n: int) -> T.Awaitable: 134 | ''' 135 | Waits for a specified number of frames to elapse. 136 | 137 | .. code-block:: 138 | 139 | await n_frames(2) 140 | 141 | If you want to wait for one frame, :func:`asynckivy.sleep` is preferable for a performance reason. 142 | 143 | .. code-block:: 144 | 145 | await sleep(0) 146 | ''' 147 | if n < 0: 148 | raise ValueError(f"Waiting for {n} frames doesn't make sense.") 149 | if not n: 150 | return 151 | 152 | task = (yield _current_task)[0][0] 153 | 154 | def callback(dt): 155 | nonlocal n 156 | n -= 1 157 | if not n: 158 | task._step() 159 | return False 160 | 161 | clock_event = Clock.schedule_interval(callback, 0) 162 | 163 | try: 164 | yield _sleep_forever 165 | finally: 166 | clock_event.cancel() 167 | -------------------------------------------------------------------------------- /src/asynckivy/_smooth_attr.py: -------------------------------------------------------------------------------- 1 | __all__ = ("smooth_attr", ) 2 | import typing as T 3 | from functools import partial 4 | import math 5 | 6 | from kivy.metrics import dp 7 | from kivy.clock import Clock 8 | from kivy.event import EventDispatcher 9 | from kivy import properties as P 10 | NUMERIC_TYPES = (P.NumericProperty, P.BoundedNumericProperty, ) 11 | SEQUENCE_TYPES = (P.ColorProperty, P.ReferenceListProperty, P.ListProperty, ) 12 | 13 | 14 | class smooth_attr: 15 | ''' 16 | Makes an attribute smoothly follow another. 17 | 18 | .. code-block:: 19 | 20 | import types 21 | 22 | widget = Widget(x=0) 23 | obj = types.SimpleNamespace(xx=100) 24 | 25 | # 'obj.xx' will smoothly follow 'widget.x'. 26 | smooth_attr(target=(widget, 'x'), follower=(obj, 'xx')) 27 | 28 | To make its effect temporary, use it with a with-statement: 29 | 30 | .. code-block:: 31 | 32 | # The effect lasts only within the with-block. 33 | with smooth_attr(...): 34 | ... 35 | 36 | A key feature of this API is that if the target value changes while being followed, 37 | the follower automatically adjusts to the new value. 38 | 39 | :param target: Must be a numeric or numeric sequence type property, that is, one of the following: 40 | 41 | * :class:`~kivy.properties.NumericProperty` 42 | * :class:`~kivy.properties.BoundedNumericProperty` 43 | * :class:`~kivy.properties.ReferenceListProperty` 44 | * :class:`~kivy.properties.ListProperty` 45 | * :class:`~kivy.properties.ColorProperty` 46 | 47 | :param speed: The speed coefficient for following. A larger value results in faster following. 48 | :param min_diff: If the difference between the target and the follower is less than this value, 49 | the follower will instantly jump to the target's value. When the target is a ``ColorProperty``, 50 | you most likely want to set this to a very small value, such as ``0.01``. Defaults to ``dp(2)``. 51 | 52 | .. versionadded:: 0.8.0 53 | ''' 54 | __slots__ = ("__exit__", ) 55 | 56 | def __init__(self, target: tuple[EventDispatcher, str], follower: tuple[T.Any, str], 57 | *, speed=10.0, min_diff=dp(2)): 58 | target_obj, target_attr = target 59 | target_desc = target_obj.property(target_attr) 60 | if isinstance(target_desc, NUMERIC_TYPES): 61 | update = self._update_follower 62 | elif isinstance(target_desc, SEQUENCE_TYPES): 63 | update = self._update_follower_ver_seq 64 | else: 65 | raise ValueError(f"Unsupported target type: {target_desc}") 66 | trigger = Clock.schedule_interval( 67 | partial(update, *target, *follower, -speed, -min_diff, min_diff), 0 68 | ) 69 | bind_uid = target_obj.fbind(target_attr, trigger) 70 | self.__exit__ = partial(self._cleanup, trigger, target_obj, target_attr, bind_uid) 71 | 72 | @staticmethod 73 | def _cleanup(trigger, target_obj, target_attr, bind_uid, *__): 74 | trigger.cancel() 75 | target_obj.unbind_uid(target_attr, bind_uid) 76 | 77 | def __enter__(self): 78 | pass 79 | 80 | def _update_follower(getattr, setattr, math_exp, target_obj, target_attr, follower_obj, follower_attr, 81 | negative_speed, min, max, dt): 82 | t_value = getattr(target_obj, target_attr) 83 | f_value = getattr(follower_obj, follower_attr) 84 | diff = f_value - t_value 85 | 86 | if min < diff < max: 87 | setattr(follower_obj, follower_attr, t_value) 88 | return False 89 | 90 | new_value = t_value + math_exp(negative_speed * dt) * diff 91 | setattr(follower_obj, follower_attr, new_value) 92 | 93 | _update_follower = partial(_update_follower, getattr, setattr, math.exp) 94 | 95 | def _update_follower_ver_seq(getattr, setattr, math_exp, seq_cls, zip, target_obj, target_attr, 96 | follower_obj, follower_attr, negative_speed, min, max, dt): 97 | t_value = getattr(target_obj, target_attr) 98 | f_value = getattr(follower_obj, follower_attr) 99 | p = math_exp(negative_speed * dt) 100 | still_going = False 101 | new_value = seq_cls( 102 | (t_elem + p * diff) if ( 103 | diff := f_elem - t_elem, 104 | _still_going := (diff <= min or max <= diff), 105 | still_going := (still_going or _still_going), 106 | ) and _still_going else t_elem 107 | for t_elem, f_elem in zip(t_value, f_value) 108 | ) 109 | setattr(follower_obj, follower_attr, new_value) 110 | return still_going 111 | 112 | _update_follower_ver_seq = partial(_update_follower_ver_seq, getattr, setattr, math.exp, tuple, zip) 113 | -------------------------------------------------------------------------------- /src/asynckivy/_threading.py: -------------------------------------------------------------------------------- 1 | __all__ = ('run_in_thread', 'run_in_executor', ) 2 | import typing as T 3 | from threading import Thread 4 | from concurrent.futures import ThreadPoolExecutor 5 | from kivy.clock import Clock 6 | import asyncgui 7 | 8 | 9 | def _wrapper(func, ev): 10 | ret = None 11 | exc = None 12 | try: 13 | ret = func() 14 | except Exception as e: 15 | exc = e 16 | finally: 17 | Clock.schedule_once(lambda __: ev.fire(ret, exc)) 18 | 19 | 20 | async def run_in_thread(func, *, daemon=None) -> T.Awaitable: 21 | ''' 22 | Creates a new thread, runs a function within it, then waits for the completion of that function. 23 | 24 | .. code-block:: 25 | 26 | return_value = await run_in_thread(func) 27 | 28 | See :ref:`io-in-asynckivy` for details. 29 | 30 | .. warning:: 31 | When the caller Task is cancelled, the ``func`` will be left running because the 32 | :mod:`threading` module does not provide any cancellation mechanism. 33 | ''' 34 | ev = asyncgui.ExclusiveEvent() 35 | Thread( 36 | name='asynckivy.run_in_thread', 37 | target=_wrapper, daemon=daemon, args=(func, ev, ), 38 | ).start() 39 | ret, exc = (await ev.wait())[0] 40 | if exc is not None: 41 | raise exc 42 | return ret 43 | 44 | 45 | async def run_in_executor(executor: ThreadPoolExecutor, func) -> T.Awaitable: 46 | ''' 47 | Runs a function within a :class:`concurrent.futures.ThreadPoolExecutor`, and waits for the completion of the 48 | function. 49 | 50 | .. code-block:: 51 | 52 | executor = ThreadPoolExecutor() 53 | ... 54 | return_value = await run_in_executor(executor, func) 55 | 56 | See :ref:`io-in-asynckivy` for details. 57 | 58 | .. warning:: 59 | When the caller Task is cancelled, the ``func`` will be left running if it has already started. 60 | This happens because :mod:`concurrent.futures` module does not provide a way to cancel a running function. 61 | (See :meth:`concurrent.futures.Future.cancel`). 62 | ''' 63 | ev = asyncgui.ExclusiveEvent() 64 | future = executor.submit(_wrapper, func, ev) 65 | try: 66 | ret, exc = (await ev.wait())[0] 67 | except asyncgui.Cancelled: 68 | future.cancel() 69 | raise 70 | assert future.done() 71 | if exc is not None: 72 | raise exc 73 | return ret 74 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from kivy.tests.fixtures import kivy_clock 3 | 4 | 5 | @pytest.fixture() 6 | def sleep_then_tick(kivy_clock): 7 | current_time = kivy_clock.time() 8 | kivy_clock.time = lambda: current_time 9 | 10 | def sleep_then_tick(duration): 11 | nonlocal current_time 12 | current_time += duration 13 | kivy_clock.tick() 14 | return sleep_then_tick 15 | -------------------------------------------------------------------------------- /tests/test_anim_attrs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def approx(): 6 | from functools import partial 7 | return partial(pytest.approx, abs=1) 8 | 9 | 10 | def test_scalar(approx, sleep_then_tick): 11 | from types import SimpleNamespace 12 | import asynckivy as ak 13 | 14 | obj = SimpleNamespace(num=0) 15 | task = ak.start(ak.anim_attrs(obj, num=100, duration=.4)) 16 | 17 | sleep_then_tick(.1) 18 | assert obj.num == approx(25) 19 | sleep_then_tick(.1) 20 | assert obj.num == approx(50) 21 | sleep_then_tick(.1) 22 | assert obj.num == approx(75) 23 | sleep_then_tick(.1) 24 | assert obj.num == approx(100) 25 | sleep_then_tick(.1) 26 | assert obj.num == approx(100) 27 | assert task.finished 28 | 29 | 30 | def test_list(approx, sleep_then_tick): 31 | from types import SimpleNamespace 32 | import asynckivy as ak 33 | 34 | obj = SimpleNamespace(list=[0, 0]) 35 | task = ak.start(ak.anim_attrs(obj, list=[100, 200], duration=.4)) 36 | 37 | sleep_then_tick(.1) 38 | assert obj.list == approx([25, 50]) 39 | sleep_then_tick(.1) 40 | assert obj.list == approx([50, 100]) 41 | sleep_then_tick(.1) 42 | assert obj.list == approx([75, 150]) 43 | sleep_then_tick(.1) 44 | assert obj.list == approx([100, 200]) 45 | sleep_then_tick(.1) 46 | assert obj.list == approx([100, 200]) 47 | assert task.finished 48 | 49 | 50 | @pytest.mark.parametrize('output_seq_type', [list, tuple]) 51 | def test_output_seq_type_parameter(sleep_then_tick, output_seq_type): 52 | from types import SimpleNamespace 53 | import asynckivy as ak 54 | 55 | obj = SimpleNamespace(size=(0, 0), pos=[0, 0]) 56 | task = ak.start(ak.anim_attrs(obj, size=[10, 10], pos=(10, 10), output_seq_type=output_seq_type)) 57 | sleep_then_tick(.1) 58 | assert type(obj.size) is output_seq_type 59 | assert type(obj.pos) is output_seq_type 60 | task.cancel() 61 | 62 | 63 | def test_cancel(approx, sleep_then_tick): 64 | from types import SimpleNamespace 65 | import asynckivy as ak 66 | 67 | obj = SimpleNamespace(num=0) 68 | task = ak.start(ak.anim_attrs(obj, num=100, duration=.4,)) 69 | 70 | sleep_then_tick(.1) 71 | assert obj.num == approx(25) 72 | sleep_then_tick(.1) 73 | assert obj.num == approx(50) 74 | task.cancel() 75 | sleep_then_tick(.1) 76 | assert obj.num == approx(50) 77 | 78 | 79 | def test_low_fps(approx, sleep_then_tick): 80 | from types import SimpleNamespace 81 | import asynckivy as ak 82 | 83 | obj = SimpleNamespace(num=0) 84 | task = ak.start(ak.anim_attrs(obj, num=100, duration=.4, step=.3)) 85 | 86 | sleep_then_tick(.1) 87 | assert obj.num == 0 88 | sleep_then_tick(.1) 89 | assert obj.num == 0 90 | sleep_then_tick(.1) 91 | assert obj.num == approx(75) 92 | sleep_then_tick(.1) 93 | assert obj.num == approx(75) 94 | sleep_then_tick(.1) 95 | assert obj.num == approx(75) 96 | sleep_then_tick(.1) 97 | assert obj.num == approx(100) 98 | assert task.finished 99 | 100 | 101 | def test_scoped_cancel(sleep_then_tick): 102 | from types import SimpleNamespace 103 | import asynckivy as ak 104 | 105 | async def async_func(ctx): 106 | obj = SimpleNamespace(num=0) 107 | async with ak.open_cancel_scope() as scope: 108 | ctx['scope'] = scope 109 | ctx['state'] = 'A' 110 | await ak.anim_attrs(obj, num=100, duration=.1,) 111 | pytest.fail() 112 | ctx['state'] = 'B' 113 | await ak.sleep_forever() 114 | ctx['state'] = 'C' 115 | 116 | ctx = {} 117 | task = ak.start(async_func(ctx)) 118 | assert ctx['state'] == 'A' 119 | ctx['scope'].cancel() 120 | assert ctx['state'] == 'B' 121 | sleep_then_tick(.2) 122 | assert ctx['state'] == 'B' 123 | task._step() 124 | assert ctx['state'] == 'C' 125 | -------------------------------------------------------------------------------- /tests/test_anim_with_xxx.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def approx(): 6 | from functools import partial 7 | return partial(pytest.approx, abs=0.004) 8 | 9 | 10 | def test_dt(approx, sleep_then_tick): 11 | import asynckivy as ak 12 | 13 | async def async_fn(result: list): 14 | async for dt in ak.anim_with_dt(): 15 | result.append(dt) 16 | 17 | result = [] 18 | durations = [.2, .5, .1, ] 19 | task = ak.start(async_fn(result)) 20 | for d in durations: 21 | sleep_then_tick(d) 22 | assert result == approx(durations) 23 | task.cancel() 24 | 25 | 26 | def test_et(approx, sleep_then_tick): 27 | from itertools import accumulate 28 | import asynckivy as ak 29 | 30 | async def async_fn(result: list): 31 | async for et in ak.anim_with_et(): 32 | result.append(et) 33 | 34 | result = [] 35 | durations = (.2, .5, .1, ) 36 | task = ak.start(async_fn(result)) 37 | for d in durations: 38 | sleep_then_tick(d) 39 | assert result == approx(list(accumulate(durations))) 40 | task.cancel() 41 | 42 | 43 | def test_dt_et(approx, sleep_then_tick): 44 | from itertools import accumulate 45 | import asynckivy as ak 46 | 47 | async def async_fn(dt_result: list, et_result: list): 48 | async for dt, et in ak.anim_with_dt_et(): 49 | dt_result.append(dt) 50 | et_result.append(et) 51 | 52 | # 'pytest.approx()' doesn't support nested lists/tuples so we put 'dt' and 'et' separately. 53 | dt_result = [] 54 | et_result = [] 55 | durations = (.2, .5, .1, ) 56 | task = ak.start(async_fn(dt_result, et_result)) 57 | for d in durations: 58 | sleep_then_tick(d) 59 | assert dt_result == approx(list(durations)) 60 | assert et_result == approx(list(accumulate(durations))) 61 | task.cancel() 62 | 63 | 64 | def test_ratio(approx, sleep_then_tick): 65 | import asynckivy as ak 66 | 67 | values = [] 68 | async def async_fn(): 69 | async for p in ak.anim_with_ratio(base=3.0): 70 | values.append(p) 71 | 72 | task = ak.start(async_fn()) 73 | for __ in range(4): 74 | sleep_then_tick(.3) 75 | assert values == approx([0.1, 0.2, 0.3, 0.4, ]) 76 | assert task.state is ak.TaskState.STARTED 77 | task.cancel() 78 | 79 | 80 | def test_ratio_zero_base(kivy_clock): 81 | import asynckivy as ak 82 | 83 | async def async_fn(): 84 | with pytest.raises(ZeroDivisionError): 85 | async for p in ak.anim_with_ratio(base=0): 86 | pass 87 | 88 | task = ak.start(async_fn()) 89 | kivy_clock.tick() 90 | assert task.finished 91 | 92 | 93 | def test_dt_et_ratio(approx, sleep_then_tick): 94 | import asynckivy as ak 95 | 96 | dt_values = [] 97 | et_values = [] 98 | p_values = [] 99 | 100 | async def async_fn(): 101 | async for dt, et, p in ak.anim_with_dt_et_ratio(base=.5): 102 | dt_values.append(dt) 103 | et_values.append(et) 104 | p_values.append(p) 105 | 106 | task = ak.start(async_fn()) 107 | for __ in range(4): 108 | sleep_then_tick(.2) 109 | assert dt_values == approx([0.2, 0.2, 0.2, 0.2, ]) 110 | assert et_values == approx([0.2, 0.4, 0.6, 0.8, ]) 111 | assert p_values == approx([0.4, 0.8, 1.2, 1.6, ]) 112 | assert task.state is ak.TaskState.STARTED 113 | task.cancel() 114 | 115 | 116 | def test_dt_et_ratio_zero_base(kivy_clock): 117 | import asynckivy as ak 118 | 119 | async def async_fn(): 120 | with pytest.raises(ZeroDivisionError): 121 | async for dt, et, p in ak.anim_with_dt_et_ratio(base=0): 122 | pass 123 | 124 | task = ak.start(async_fn()) 125 | kivy_clock.tick() 126 | assert task.finished 127 | -------------------------------------------------------------------------------- /tests/test_event.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def ed_cls(): 6 | from kivy.event import EventDispatcher 7 | class ConcreteEventDispatcher(EventDispatcher): 8 | __events__ = ('on_test', 'on_test2', ) 9 | def on_test(self, *args, **kwargs): 10 | pass 11 | def on_test2(self, *args, **kwargs): 12 | pass 13 | return ConcreteEventDispatcher 14 | 15 | 16 | @pytest.fixture() 17 | def ed(ed_cls): 18 | return ed_cls() 19 | 20 | 21 | def test_properly_unbound(ed): 22 | import asynckivy as ak 23 | async def _test(): 24 | nonlocal state 25 | state = 'A' 26 | await ak.event(ed, 'on_test') 27 | state = 'B' 28 | await ak.event(ed, 'on_test2') 29 | state = 'C' 30 | await ak.event(ed, 'on_test') 31 | state = 'D' 32 | state = '' 33 | ak.start(_test()) 34 | assert state == 'A' 35 | ed.dispatch('on_test') 36 | assert state == 'B' 37 | ed.dispatch('on_test') 38 | assert state == 'B' 39 | ed.dispatch('on_test2') 40 | assert state == 'C' 41 | ed.dispatch('on_test') 42 | assert state == 'D' 43 | 44 | 45 | def test_event_parameter(ed): 46 | import asynckivy as ak 47 | 48 | async def _test(): 49 | r = await ak.event(ed, 'on_test') 50 | assert r == (ed, 1, 2, ) 51 | r = await ak.event(ed, 'on_test') 52 | assert r == (ed, 3, 4, ) # kwarg is ignored 53 | 54 | task = ak.start(_test()) 55 | assert not task.finished 56 | ed.dispatch('on_test', 1, 2) 57 | assert not task.finished 58 | ed.dispatch('on_test', 3, 4, kwarg='A') 59 | assert task.finished 60 | 61 | 62 | def test_filter(ed): 63 | import asynckivy as ak 64 | 65 | async def _test(): 66 | await ak.event( 67 | ed, 'on_test', 68 | filter=lambda *args: args == (ed, 3, 4, ) 69 | ) 70 | 71 | task = ak.start(_test()) 72 | assert not task.finished 73 | ed.dispatch('on_test', 1, 2) 74 | assert not task.finished 75 | ed.dispatch('on_test', 3, 4) 76 | assert task.finished 77 | 78 | 79 | def test_stop_dispatching(ed): 80 | import asynckivy as ak 81 | 82 | async def _test(): 83 | await ak.event(ed, 'on_test') 84 | await ak.event(ed, 'on_test', stop_dispatching=True) 85 | await ak.event(ed, 'on_test') 86 | 87 | n = 0 88 | def increament(*args): 89 | nonlocal n 90 | n += 1 91 | ed.bind(on_test=increament) 92 | task = ak.start(_test()) 93 | assert n == 0 94 | assert not task.finished 95 | ed.dispatch('on_test') 96 | assert n == 1 97 | assert not task.finished 98 | ed.dispatch('on_test') 99 | assert n == 1 100 | assert not task.finished 101 | ed.dispatch('on_test') 102 | assert n == 2 103 | assert task.finished 104 | 105 | 106 | def test_cancel(ed): 107 | import asynckivy as ak 108 | 109 | async def _test(ed): 110 | def filter_func(*args): 111 | nonlocal called; called = True 112 | return True 113 | await ak.event(ed, 'on_test', filter=filter_func) 114 | 115 | called = False 116 | task = ak.start(_test(ed)) 117 | assert not task.finished 118 | assert not called 119 | task.close() 120 | assert not task.finished 121 | assert not called 122 | ed.dispatch('on_test') 123 | assert not task.finished 124 | assert not called 125 | -------------------------------------------------------------------------------- /tests/test_event_freq.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def ed_cls(): 6 | from kivy.event import EventDispatcher 7 | class ConcreteEventDispatcher(EventDispatcher): 8 | __events__ = ('on_test', 'on_test2', ) 9 | def on_test(self, *args, **kwargs): 10 | pass 11 | def on_test2(self, *args, **kwargs): 12 | pass 13 | return ConcreteEventDispatcher 14 | 15 | 16 | @pytest.fixture() 17 | def ed(ed_cls): 18 | return ed_cls() 19 | 20 | 21 | def test_properly_cleanuped(ed): 22 | import asynckivy as ak 23 | async def async_fn(): 24 | async with ak.event_freq(ed, 'on_test') as on_test: 25 | await on_test() 26 | await on_test() 27 | await ak.sleep_forever() 28 | 29 | task = ak.start(async_fn()) 30 | ed.dispatch('on_test') 31 | assert not task.finished 32 | ed.dispatch('on_test') 33 | assert not task.finished 34 | ed.dispatch('on_test') 35 | assert not task.finished 36 | task._step() 37 | assert task.finished 38 | 39 | 40 | def test_event_parameters(ed): 41 | import asynckivy as ak 42 | 43 | async def async_fn(): 44 | async with ak.event_freq(ed, 'on_test') as on_test: 45 | assert (ed, 1, 2, ) == await on_test() 46 | assert (ed, 3, 4, ) == await on_test() # kwarg is ignored 47 | 48 | task = ak.start(async_fn()) 49 | assert not task.finished 50 | ed.dispatch('on_test', 1, 2) 51 | assert not task.finished 52 | ed.dispatch('on_test', 3, 4, kwarg='A') 53 | assert task.finished 54 | 55 | 56 | def test_filter(ed): 57 | import asynckivy as ak 58 | 59 | async def async_fn(): 60 | async with ak.event_freq(ed, 'on_test', filter=lambda *args: args == (ed, 3, 4, )) as on_test: 61 | await on_test() 62 | 63 | task = ak.start(async_fn()) 64 | assert not task.finished 65 | ed.dispatch('on_test', 1, 2) 66 | assert not task.finished 67 | ed.dispatch('on_test', 3, 4) 68 | assert task.finished 69 | 70 | 71 | def test_stop_dispatching(ed): 72 | import asynckivy as ak 73 | 74 | called = [] 75 | 76 | async def async_fn(): 77 | ed.bind(on_test=lambda *args: called.append(1)) 78 | async with ak.event_freq(ed, 'on_test', stop_dispatching=True) as on_test: 79 | await on_test() 80 | 81 | task = ak.start(async_fn()) 82 | assert not called 83 | ed.dispatch('on_test') 84 | assert not called 85 | assert task.finished 86 | ed.dispatch('on_test') 87 | assert called 88 | 89 | 90 | def test_cancel(ed): 91 | import asynckivy as ak 92 | 93 | async def async_fn(ed): 94 | def filter_func(*args): 95 | nonlocal called; called = True 96 | return True 97 | async with ak.event_freq(ed, 'on_test', filter=filter_func) as on_test: 98 | await on_test() 99 | 100 | called = False 101 | task = ak.start(async_fn(ed)) 102 | assert not task.finished 103 | assert not called 104 | task.close() 105 | assert not task.finished 106 | assert not called 107 | ed.dispatch('on_test') 108 | assert not task.finished 109 | assert not called 110 | -------------------------------------------------------------------------------- /tests/test_fade_transition.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def approx(): 6 | from functools import partial 7 | return partial(pytest.approx, abs=0.01) 8 | 9 | 10 | def test_run_normally(approx, sleep_then_tick): 11 | from types import SimpleNamespace 12 | import asynckivy as ak 13 | 14 | async def job(w1, w2): 15 | async with ak.fade_transition(w1, w2): 16 | pass 17 | 18 | w1 = SimpleNamespace(opacity=1) 19 | w2 = SimpleNamespace(opacity=2) 20 | task = ak.start(job(w1, w2)) 21 | sleep_then_tick(.1) 22 | assert w1.opacity == approx(.8) 23 | assert w2.opacity == approx(1.6) 24 | sleep_then_tick(.4) 25 | assert w1.opacity == approx(0) 26 | assert w2.opacity == approx(0) 27 | sleep_then_tick(.1) 28 | assert w1.opacity == approx(.2) 29 | assert w2.opacity == approx(.4) 30 | sleep_then_tick(.4) 31 | assert w1.opacity == approx(1) 32 | assert w2.opacity == approx(2) 33 | sleep_then_tick(.2) 34 | assert w1.opacity == 1 35 | assert w2.opacity == 2 36 | assert task.finished 37 | 38 | 39 | def test_cancel(approx, sleep_then_tick): 40 | from types import SimpleNamespace 41 | import asynckivy as ak 42 | 43 | async def job(w1, w2): 44 | async with ak.fade_transition(w1, w2): 45 | pass 46 | 47 | w1 = SimpleNamespace(opacity=1) 48 | w2 = SimpleNamespace(opacity=2) 49 | task = ak.start(job(w1, w2)) 50 | sleep_then_tick(.1) 51 | assert w1.opacity == approx(.8) 52 | assert w2.opacity == approx(1.6) 53 | task.cancel() 54 | assert w1.opacity == 1 55 | assert w2.opacity == 2 56 | -------------------------------------------------------------------------------- /tests/test_interpolate.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def approx(): 6 | from functools import partial 7 | return partial(pytest.approx, abs=1) 8 | 9 | 10 | def test_complete_the_iteration(approx, sleep_then_tick): 11 | import asynckivy as ak 12 | 13 | async def job(): 14 | l = [v async for v in ak.interpolate(start=0, end=100)] 15 | assert l == approx([0, 30, 60, 90, 100]) 16 | 17 | task = ak.start(job()) 18 | for __ in range(4): 19 | sleep_then_tick(.3) 20 | assert task.finished 21 | 22 | 23 | def test_break_during_the_iteration(approx, sleep_then_tick): 24 | import asynckivy as ak 25 | 26 | async def job(): 27 | l = [] 28 | async for v in ak.interpolate(start=0, end=100): 29 | l.append(v) 30 | if v > 50: 31 | break 32 | assert l == approx([0, 30, 60, ]) 33 | 34 | task = ak.start(job()) 35 | for __ in range(2): 36 | sleep_then_tick(.3) 37 | assert task.finished 38 | 39 | 40 | def test_zero_duration(approx, sleep_then_tick): 41 | import asynckivy as ak 42 | 43 | async def job(): 44 | l = [v async for v in ak.interpolate(start=0, end=100, duration=0)] 45 | assert l == approx([0, 100]) 46 | 47 | task = ak.start(job()) 48 | sleep_then_tick(.1) 49 | assert task.finished 50 | -------------------------------------------------------------------------------- /tests/test_interpolate_seq.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def approx(): 6 | from functools import partial 7 | return partial(pytest.approx, abs=1) 8 | 9 | 10 | 11 | def test_complete_the_iterations(approx, sleep_then_tick): 12 | import asynckivy as ak 13 | values = [] 14 | 15 | async def async_fn(): 16 | async for v in ak.interpolate_seq([0, 100], [100, 0], duration=1.0): 17 | values.extend(v) 18 | 19 | task = ak.start(async_fn()) 20 | assert values == approx([0, 100]) ; values.clear() 21 | sleep_then_tick(0.3) 22 | assert values == approx([30, 70]) ; values.clear() 23 | sleep_then_tick(0.3) 24 | assert values == approx([60, 40]) ; values.clear() 25 | sleep_then_tick(0.3) 26 | assert values == approx([90, 10]) ; values.clear() 27 | sleep_then_tick(0.3) 28 | assert values == approx([100, 0]) ; values.clear() 29 | assert task.finished 30 | 31 | 32 | @pytest.mark.parametrize('step', [0, 10]) 33 | def test_zero_duration(kivy_clock, step): 34 | import asynckivy as ak 35 | values = [] 36 | 37 | async def async_fn(): 38 | async for v in ak.interpolate_seq([0, 100], [100, 0], duration=0, step=step): 39 | values.extend(v) 40 | 41 | task = ak.start(async_fn()) 42 | assert values == [0, 100] ; values.clear() 43 | kivy_clock.tick() 44 | assert values == [100, 0] ; values.clear() 45 | assert task.finished 46 | 47 | 48 | def test_break_during_the_iterations(approx, sleep_then_tick): 49 | import asynckivy as ak 50 | values = [] 51 | 52 | async def async_fn(): 53 | async for v in ak.interpolate_seq([0, 100], [100, 0], duration=1.0): 54 | values.extend(v) 55 | if v[0] > 50: 56 | break 57 | 58 | task = ak.start(async_fn()) 59 | assert values == approx([0, 100]) ; values.clear() 60 | sleep_then_tick(.3) 61 | assert values == approx([30, 70]) ; values.clear() 62 | sleep_then_tick(.3) 63 | assert values == approx([60, 40]) ; values.clear() 64 | assert task.finished 65 | -------------------------------------------------------------------------------- /tests/test_n_frames.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize("n", range(3)) 5 | def test_non_negative_number_of_frames(kivy_clock, n): 6 | import asynckivy as ak 7 | 8 | task = ak.start(ak.n_frames(n)) 9 | for __ in range(n): 10 | assert not task.finished 11 | kivy_clock.tick() 12 | assert task.finished 13 | 14 | 15 | def test_cancel(kivy_clock): 16 | import asynckivy as ak 17 | 18 | task = ak.start(ak.n_frames(2)) 19 | assert not task.finished 20 | kivy_clock.tick() 21 | assert not task.finished 22 | task.cancel() 23 | assert task.cancelled 24 | kivy_clock.tick() 25 | kivy_clock.tick() 26 | kivy_clock.tick() 27 | 28 | 29 | def test_negative_number_of_frames(): 30 | import asynckivy as ak 31 | 32 | with pytest.raises(ValueError): 33 | ak.start(ak.n_frames(-2)) 34 | 35 | 36 | def test_scoped_cancel(kivy_clock): 37 | import asynckivy as ak 38 | TS = ak.TaskState 39 | 40 | async def async_fn(ctx): 41 | async with ak.open_cancel_scope() as scope: 42 | ctx['scope'] = scope 43 | await ak.n_frames(1) 44 | pytest.fail() 45 | await ak.sleep_forever() 46 | 47 | ctx = {} 48 | task = ak.start(async_fn(ctx)) 49 | assert task.state is TS.STARTED 50 | ctx['scope'].cancel() 51 | assert task.state is TS.STARTED 52 | kivy_clock.tick() 53 | assert task.state is TS.STARTED 54 | task._step() 55 | assert task.state is TS.FINISHED 56 | -------------------------------------------------------------------------------- /tests/test_rest_of_touch_events.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize('n_touch_moves', [0, 1, 10]) 5 | def test_a_number_of_touch_moves(n_touch_moves): 6 | from kivy.uix.widget import Widget 7 | from kivy.tests.common import UnitTestTouch 8 | import asynckivy as ak 9 | 10 | async def async_fn(w, t): 11 | n = 0 12 | async for __ in ak.rest_of_touch_events(w, t): 13 | n += 1 14 | assert n == n_touch_moves 15 | 16 | w = Widget() 17 | t = UnitTestTouch(0, 0) 18 | task = ak.start(async_fn(w, t)) 19 | for __ in range(n_touch_moves): 20 | t.grab_current = None 21 | w.dispatch('on_touch_move', t) 22 | t.grab_current = w 23 | w.dispatch('on_touch_move', t) 24 | t.grab_current = None 25 | w.dispatch('on_touch_up', t) 26 | t.grab_current = w 27 | w.dispatch('on_touch_up', t) 28 | assert task.finished 29 | 30 | 31 | def test_break_during_a_for_loop(): 32 | from kivy.uix.widget import Widget 33 | from kivy.tests.common import UnitTestTouch 34 | import asynckivy as ak 35 | 36 | async def async_fn(w, t): 37 | import weakref 38 | nonlocal n_touch_moves 39 | weak_w = weakref.ref(w) 40 | assert weak_w not in t.grab_list 41 | async for __ in ak.rest_of_touch_events(w, t): 42 | assert weak_w in t.grab_list 43 | n_touch_moves += 1 44 | if n_touch_moves == 2: 45 | break 46 | assert weak_w not in t.grab_list 47 | await ak.event(w, 'on_touch_up') 48 | 49 | n_touch_moves = 0 50 | w = Widget() 51 | t = UnitTestTouch(0, 0) 52 | task = ak.start(async_fn(w, t)) 53 | for expected in (1, 2, 2, ): 54 | t.grab_current = None 55 | w.dispatch('on_touch_move', t) 56 | t.grab_current = w 57 | w.dispatch('on_touch_move', t) 58 | assert n_touch_moves == expected 59 | assert not task.finished 60 | t.grab_current = None 61 | w.dispatch('on_touch_up', t) 62 | t.grab_current = w 63 | w.dispatch('on_touch_up', t) 64 | assert n_touch_moves == 2 65 | assert task.finished 66 | 67 | 68 | @pytest.mark.parametrize( 69 | 'stop_dispatching, expectation', [ 70 | (True, [0, 0, 0, ], ), 71 | (False, [1, 2, 1, ], ), 72 | ]) 73 | def test_stop_dispatching(stop_dispatching, expectation): 74 | from kivy.uix.widget import Widget 75 | from kivy.tests.common import UnitTestTouch 76 | import asynckivy as ak 77 | 78 | async def async_fn(parent, t): 79 | async for __ in ak.rest_of_touch_events(parent, t, stop_dispatching=stop_dispatching): 80 | pass 81 | 82 | n_touches = {'move': 0, 'up': 0, } 83 | def on_touch_move(*args): 84 | n_touches['move'] += 1 85 | def on_touch_up(*args): 86 | n_touches['up'] += 1 87 | 88 | parent = Widget() 89 | child = Widget( 90 | on_touch_move=on_touch_move, 91 | on_touch_up=on_touch_up, 92 | ) 93 | parent.add_widget(child) 94 | t = UnitTestTouch(0, 0) 95 | task = ak.start(async_fn(parent, t)) 96 | 97 | for i in range(2): 98 | t.grab_current = None 99 | parent.dispatch('on_touch_move', t) 100 | t.grab_current = parent 101 | parent.dispatch('on_touch_move', t) 102 | assert n_touches['move'] == expectation[i] 103 | t.grab_current = None 104 | parent.dispatch('on_touch_up', t) 105 | t.grab_current = parent 106 | parent.dispatch('on_touch_up', t) 107 | assert n_touches['up'] == expectation[2] 108 | assert task.finished 109 | 110 | 111 | @pytest.mark.parametrize('timeout', (.2, 1.)) 112 | @pytest.mark.parametrize('actually_ended', (True, False)) 113 | def test_a_touch_that_might_have_already_ended(sleep_then_tick, timeout, actually_ended): 114 | from contextlib import nullcontext 115 | from kivy.uix.widget import Widget 116 | from kivy.tests.common import UnitTestTouch 117 | import asynckivy as ak 118 | 119 | async def async_fn(w, t): 120 | with pytest.raises(ak.MotionEventAlreadyEndedError) if actually_ended else nullcontext(): 121 | async for __ in ak.rest_of_touch_events(w, t, timeout=timeout): 122 | pass 123 | 124 | w = Widget() 125 | t = UnitTestTouch(0, 0) 126 | t.time_end = 1 # something other than -1 127 | task = ak.start(async_fn(w, t)) 128 | 129 | if actually_ended: 130 | sleep_then_tick(timeout) 131 | else: 132 | t.grab_current = None 133 | w.dispatch('on_touch_up', t) 134 | t.grab_current = w 135 | w.dispatch('on_touch_up', t) 136 | assert task.finished 137 | -------------------------------------------------------------------------------- /tests/test_run_in_executor.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from concurrent.futures import ThreadPoolExecutor 3 | import time 4 | import threading 5 | 6 | 7 | def test_thread_id(kivy_clock): 8 | import asynckivy as ak 9 | 10 | async def job(executor): 11 | before = threading.get_ident() 12 | await ak.run_in_executor(executor, lambda: None) 13 | after = threading.get_ident() 14 | assert before == after 15 | 16 | 17 | with ThreadPoolExecutor() as executor: 18 | task = ak.start(job(executor)) 19 | time.sleep(.01) 20 | assert not task.finished 21 | kivy_clock.tick() 22 | assert task.finished 23 | 24 | 25 | def test_propagate_exception(kivy_clock): 26 | import asynckivy as ak 27 | 28 | async def job(executor): 29 | with pytest.raises(ZeroDivisionError): 30 | await ak.run_in_executor(executor, lambda: 1 / 0) 31 | 32 | with ThreadPoolExecutor() as executor: 33 | task = ak.start(job(executor)) 34 | time.sleep(.01) 35 | assert not task.finished 36 | kivy_clock.tick() 37 | assert task.finished 38 | 39 | 40 | def test_no_exception(kivy_clock): 41 | import asynckivy as ak 42 | 43 | async def job(executor): 44 | assert 'A' == await ak.run_in_executor(executor, lambda: 'A') 45 | 46 | with ThreadPoolExecutor() as executor: 47 | task = ak.start(job(executor)) 48 | time.sleep(.01) 49 | assert not task.finished 50 | kivy_clock.tick() 51 | assert task.finished 52 | 53 | 54 | def test_cancel_before_start_executing(kivy_clock): 55 | import time 56 | import asynckivy as ak 57 | 58 | e = ak.StatefulEvent() 59 | 60 | async def job(executor): 61 | await ak.run_in_executor(executor, e.fire) 62 | 63 | with ThreadPoolExecutor(max_workers=1) as executor: 64 | executor.submit(time.sleep, .1) 65 | task = ak.start(job(executor)) 66 | time.sleep(.02) 67 | assert not task.finished 68 | assert not e.is_fired 69 | kivy_clock.tick() 70 | task.cancel() 71 | assert task.cancelled 72 | assert not e.is_fired 73 | time.sleep(.2) 74 | assert not e.is_fired 75 | -------------------------------------------------------------------------------- /tests/test_run_in_thread.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import threading 3 | import time 4 | 5 | 6 | @pytest.mark.parametrize('daemon', (True, False)) 7 | def test_thread_id(daemon, kivy_clock): 8 | import asynckivy as ak 9 | 10 | async def job(): 11 | before = threading.get_ident() 12 | await ak.run_in_thread(lambda: None, daemon=daemon) 13 | after = threading.get_ident() 14 | assert before == after 15 | 16 | task = ak.start(job()) 17 | time.sleep(.01) 18 | assert not task.finished 19 | kivy_clock.tick() 20 | assert task.finished 21 | 22 | 23 | @pytest.mark.parametrize('daemon', (True, False)) 24 | def test_propagate_exception(daemon, kivy_clock): 25 | import asynckivy as ak 26 | 27 | async def job(): 28 | with pytest.raises(ZeroDivisionError): 29 | await ak.run_in_thread(lambda: 1 / 0, daemon=daemon) 30 | 31 | task = ak.start(job()) 32 | time.sleep(.01) 33 | assert not task.finished 34 | kivy_clock.tick() 35 | assert task.finished 36 | 37 | 38 | @pytest.mark.parametrize('daemon', (True, False)) 39 | def test_no_exception(daemon, kivy_clock): 40 | import asynckivy as ak 41 | 42 | async def job(): 43 | assert 'A' == await ak.run_in_thread(lambda: 'A', daemon=daemon) 44 | 45 | task = ak.start(job()) 46 | time.sleep(.01) 47 | assert not task.finished 48 | kivy_clock.tick() 49 | assert task.finished 50 | -------------------------------------------------------------------------------- /tests/test_sleep.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | p_free = pytest.mark.parametrize("free", (True, False, )) 4 | 5 | @p_free 6 | def test_sleep(kivy_clock, sleep_then_tick, free): 7 | import asynckivy as ak 8 | 9 | if free and not hasattr(kivy_clock, 'create_trigger_free'): 10 | pytest.skip("free-type Clock is not available") 11 | task = ak.start(ak.sleep_free(.1) if free else ak.sleep(.1)) 12 | assert not task.finished 13 | sleep_then_tick(.05) 14 | assert not task.finished 15 | sleep_then_tick(.06) 16 | assert task.finished 17 | 18 | 19 | def test_repeat_sleeping(sleep_then_tick): 20 | import asynckivy as ak 21 | 22 | async def async_fn(): 23 | nonlocal task_state 24 | async with ak.repeat_sleeping(step=.5) as sleep: 25 | task_state = 'A' 26 | await sleep() 27 | task_state = 'B' 28 | await sleep() 29 | task_state = 'C' 30 | 31 | task_state = None 32 | task = ak.start(async_fn()) 33 | sleep_then_tick(.2) 34 | assert task_state == 'A' 35 | assert not task.finished 36 | sleep_then_tick(.5) 37 | assert task_state == 'B' 38 | assert not task.finished 39 | sleep_then_tick(.5) 40 | assert task_state == 'C' 41 | assert task.finished 42 | 43 | 44 | @p_free 45 | def test_sleep_cancel(kivy_clock, free): 46 | import asynckivy as ak 47 | 48 | if free and not hasattr(kivy_clock, 'create_trigger_free'): 49 | pytest.skip("free-type Clock is not available") 50 | 51 | async def async_fn(ctx): 52 | async with ak.open_cancel_scope() as scope: 53 | ctx['scope'] = scope 54 | ctx['state'] = 'A' 55 | await (ak.sleep_free(0) if free else ak.sleep(0)) 56 | pytest.fail() 57 | ctx['state'] = 'B' 58 | await ak.sleep_forever() 59 | ctx['state'] = 'C' 60 | 61 | ctx = {} 62 | task = ak.start(async_fn(ctx)) 63 | assert ctx['state'] == 'A' 64 | ctx['scope'].cancel() 65 | assert ctx['state'] == 'B' 66 | kivy_clock.tick() 67 | assert ctx['state'] == 'B' 68 | task._step() 69 | assert ctx['state'] == 'C' 70 | 71 | 72 | def test_cancel_repeat_sleeping(kivy_clock): 73 | import asynckivy as ak 74 | 75 | async def async_fn(ctx): 76 | async with ak.open_cancel_scope() as scope: 77 | ctx['scope'] = scope 78 | ctx['state'] = 'A' 79 | async with ak.repeat_sleeping(step=0) as sleep: 80 | await sleep() 81 | pytest.fail() 82 | ctx['state'] = 'B' 83 | await ak.sleep_forever() 84 | ctx['state'] = 'C' 85 | 86 | ctx = {} 87 | task = ak.start(async_fn(ctx)) 88 | assert ctx['state'] == 'A' 89 | ctx['scope'].cancel() 90 | assert ctx['state'] == 'B' 91 | kivy_clock.tick() 92 | assert ctx['state'] == 'B' 93 | task._step() 94 | assert ctx['state'] == 'C' 95 | -------------------------------------------------------------------------------- /tests/test_suppress_event.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def frog_cls(): 6 | from kivy.event import EventDispatcher 7 | from kivy.properties import NumericProperty 8 | 9 | class Frog(EventDispatcher): 10 | __events__ = ('on_jump', ) 11 | n_jumped = NumericProperty(0) 12 | 13 | def on_jump(self, distance=0): 14 | self.n_jumped += 1 15 | 16 | return Frog 17 | 18 | 19 | @pytest.fixture() 20 | def frog(frog_cls): 21 | return frog_cls() 22 | 23 | 24 | def test_simple_use(frog): 25 | from asynckivy import suppress_event 26 | 27 | assert frog.n_jumped == 0 28 | with suppress_event(frog, 'on_jump'): 29 | frog.dispatch('on_jump') 30 | assert frog.n_jumped == 0 31 | frog.dispatch('on_jump') 32 | assert frog.n_jumped == 1 33 | with suppress_event(frog, 'on_jump'): 34 | frog.dispatch('on_jump') 35 | assert frog.n_jumped == 1 36 | frog.dispatch('on_jump') 37 | assert frog.n_jumped == 2 38 | 39 | 40 | def test_filter(frog): 41 | from asynckivy import suppress_event 42 | 43 | with suppress_event(frog, 'on_jump', filter=lambda __, distance: distance > 1): 44 | frog.dispatch('on_jump', distance=2) 45 | assert frog.n_jumped == 0 46 | frog.dispatch('on_jump', distance=0) 47 | assert frog.n_jumped == 1 48 | frog.dispatch('on_jump', distance=2) 49 | assert frog.n_jumped == 1 50 | frog.dispatch('on_jump', distance=0) 51 | assert frog.n_jumped == 2 52 | 53 | frog.dispatch('on_jump', distance=2) 54 | assert frog.n_jumped == 3 55 | 56 | 57 | def test_bind_a_callback_after_entering(frog): 58 | from asynckivy import suppress_event 59 | 60 | called = False 61 | 62 | def callback(frog, distance=0): 63 | nonlocal called; called = True 64 | 65 | with suppress_event(frog, 'on_jump'): 66 | frog.bind(on_jump=callback) 67 | frog.dispatch('on_jump') 68 | assert called 69 | assert frog.n_jumped == 0 70 | -------------------------------------------------------------------------------- /tests/test_sync_attr.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def human_cls(): 6 | from kivy.event import EventDispatcher 7 | from kivy.properties import NumericProperty 8 | 9 | class Human(EventDispatcher): 10 | age = NumericProperty(10) 11 | 12 | return Human 13 | 14 | 15 | @pytest.fixture() 16 | def human(human_cls): 17 | return human_cls() 18 | 19 | 20 | def test_sync_attr(human): 21 | import types 22 | import asynckivy as ak 23 | 24 | obj = types.SimpleNamespace() 25 | with ak.sync_attr(from_=(human, 'age'), to_=(obj, 'AGE')): 26 | assert obj.AGE == 10 27 | human.age = 2 28 | assert obj.AGE == 2 29 | human.age = 0 30 | assert obj.AGE == 0 31 | human.age = 1 32 | assert obj.AGE == 0 33 | 34 | 35 | def test_sync_attrs(human): 36 | import types 37 | import asynckivy as ak 38 | 39 | obj = types.SimpleNamespace() 40 | with ak.sync_attrs((human, 'age'), (obj, 'AGE'), (obj, 'age')): 41 | assert obj.AGE == 10 42 | assert obj.age == 10 43 | human.age = 2 44 | assert obj.AGE == 2 45 | assert obj.age == 2 46 | human.age = 0 47 | assert obj.AGE == 0 48 | assert obj.age == 0 49 | human.age = 1 50 | assert obj.AGE == 0 51 | assert obj.age == 0 52 | -------------------------------------------------------------------------------- /tests/test_transform.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture() 5 | def widget(): 6 | from kivy.uix.widget import Widget 7 | from kivy.graphics import Color 8 | w = Widget() 9 | w.canvas.add(Color()) 10 | return w 11 | 12 | 13 | def list_children(canvas): 14 | return [inst.__class__.__name__ for inst in canvas.children] 15 | 16 | 17 | def test_just_confirm_how_a_before_group_and_an_after_group_work(widget): 18 | c = widget.canvas 19 | assert list_children(c) == ['Color', ] 20 | c.before 21 | assert list_children(c) == ['CanvasBase', 'Color', ] 22 | c.after 23 | assert list_children(c) == ['CanvasBase', 'Color', 'CanvasBase', ] 24 | 25 | 26 | @pytest.mark.parametrize('has_before', (True, False, )) 27 | @pytest.mark.parametrize('has_after', (True, False, )) 28 | def test_use_outer_canvas(widget, has_before, has_after): 29 | from asynckivy import transform 30 | c = widget.canvas 31 | if has_before: c.before 32 | if has_after: c.after 33 | with transform(widget, use_outer_canvas=True): 34 | assert c.has_before 35 | assert c.has_after 36 | assert list_children(c) == ['CanvasBase', 'Color', 'CanvasBase'] 37 | assert list_children(c.before) == ['PushMatrix', 'InstructionGroup', ] 38 | assert list_children(c.after) == ['PopMatrix', ] 39 | assert list_children(c) == ['CanvasBase', 'Color', 'CanvasBase', ] 40 | assert list_children(c.before) == [] 41 | assert list_children(c.after) == [] 42 | 43 | 44 | @pytest.mark.parametrize('has_before', (True, False, )) 45 | def test_use_inner_canvas__has_after(widget, has_before): 46 | from asynckivy import transform 47 | c = widget.canvas 48 | c.after 49 | if has_before: c.before 50 | with transform(widget, use_outer_canvas=False): 51 | assert c.has_before 52 | assert c.has_after 53 | assert list_children(c) == ['CanvasBase', 'PushMatrix', 'InstructionGroup', 'Color', 'PopMatrix', 'CanvasBase', ] 54 | assert list_children(c.before) == [] 55 | assert list_children(c.after) == [] 56 | assert list_children(c) == ['CanvasBase', 'Color', 'CanvasBase', ] 57 | assert list_children(c.before) == [] 58 | assert list_children(c.after) == [] 59 | 60 | 61 | @pytest.mark.parametrize('has_before', (True, False, )) 62 | def test_use_inner_canvas__no_after(widget, has_before): 63 | from asynckivy import transform 64 | c = widget.canvas 65 | if has_before: c.before 66 | with transform(widget, use_outer_canvas=False): 67 | assert c.has_before 68 | assert not c.has_after 69 | assert list_children(c) == ['CanvasBase', 'PushMatrix', 'InstructionGroup', 'Color', 'PopMatrix', ] 70 | assert list_children(c.before) == [] 71 | assert not c.has_after 72 | assert list_children(c) == ['CanvasBase', 'Color', ] 73 | assert list_children(c.before) == [] 74 | --------------------------------------------------------------------------------