From 7361cc8723e3282ebe13d7d0f0fee122ef5e03a3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 14:37:49 +0000 Subject: [PATCH 01/10] sleep --- src/textual/_time.py | 28 +++++++++++++++++++++------- src/textual/_win_sleep.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 src/textual/_win_sleep.py diff --git a/src/textual/_time.py b/src/textual/_time.py index f332469468..19f942a615 100644 --- a/src/textual/_time.py +++ b/src/textual/_time.py @@ -1,4 +1,5 @@ import platform +import sys from asyncio import sleep as asyncio_sleep, get_running_loop from time import monotonic, perf_counter, sleep as time_sleep @@ -14,17 +15,30 @@ time = monotonic +import ctypes + +winmm = ctypes.WinDLL("winmm") + + if WINDOWS: - async def sleep(sleep_for: float) -> None: - """An asyncio sleep. + if sys.version_info >= (3, 11, 0): + + async def sleep(sleep_for: float) -> None: + """An asyncio sleep. + + On Windows this achieves a better granularity that asyncio.sleep + + Args: + sleep_for (float): Seconds to sleep for. + """ + await get_running_loop().run_in_executor(None, time_sleep, sleep_for) - On Windows this achieves a better granularity that asyncio.sleep + else: + from ._win_sleep import sleep as win_sleep - Args: - sleep_for (float): Seconds to sleep for. - """ - await get_running_loop().run_in_executor(None, time_sleep, sleep_for) + async def sleep(sleep_for: float) -> None: + await get_running_loop().run_in_executor(None, win_sleep, sleep_for) else: sleep = asyncio_sleep diff --git a/src/textual/_win_sleep.py b/src/textual/_win_sleep.py new file mode 100644 index 0000000000..a542fe62e0 --- /dev/null +++ b/src/textual/_win_sleep.py @@ -0,0 +1,34 @@ +import ctypes +from ctypes.wintypes import LARGE_INTEGER +from time import sleep as time_sleep + +__all__ = ["sleep"] + +kernel32 = ctypes.windll.kernel32 + +INFINITE = 0xFFFFFFFF +WAIT_FAILED = 0xFFFFFFFF +CREATE_WAITABLE_TIMER_HIGH_RESOLUTION = 0x00000002 + + +def sleep(sleep_for: float) -> None: + handle = kernel32.CreateWaitableTimerExW( + None, None, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, 0x1F0003 + ) + if not handle: + time_sleep(sleep_for) + return + + if not kernel32.SetWaitableTimer( + handle, + ctypes.byref(LARGE_INTEGER(int(sleep_for * -10000))), + 0, + None, + None, + 0, + ): + time_sleep(sleep_for) + return + + kernel32.WaitForSingleObject(handle, INFINITE) + kernel32.CancelWaitableTimer(handle) From 24d92d18c707f0e7553cc7a1f36396fb7f9a242e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 14:40:09 +0000 Subject: [PATCH 02/10] version bump --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9728e8f50c..05afcae95e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.9.1] - 2022-12-30 + +### Added + +- Added textual._win_sleep for Python on Windows < 3.11 + ## [0.9.0] - 2022-12-30 ### Added diff --git a/pyproject.toml b/pyproject.toml index 700eaad447..444e5461c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.9.0" +version = "0.9.1" homepage = "https://github.com/Textualize/textual" description = "Modern Text User Interface framework" authors = ["Will McGugan "] From e764e6757faaf3e20430ec04e426d1d3e88b86c3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 14:45:28 +0000 Subject: [PATCH 03/10] remove ctypes --- src/textual/_time.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/textual/_time.py b/src/textual/_time.py index 19f942a615..d6a8403d16 100644 --- a/src/textual/_time.py +++ b/src/textual/_time.py @@ -14,12 +14,6 @@ else: time = monotonic - -import ctypes - -winmm = ctypes.WinDLL("winmm") - - if WINDOWS: if sys.version_info >= (3, 11, 0): From efa16e2ab0e804b3908ce5ec16277ee7e2753bb6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 14:48:05 +0000 Subject: [PATCH 04/10] changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05afcae95e..0ee7b9a673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added -- Added textual._win_sleep for Python on Windows < 3.11 +- Added textual._win_sleep for Python on Windows < 3.11 https://github.com/Textualize/textual/pull/1457 ## [0.9.0] - 2022-12-30 @@ -322,6 +322,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.9.1]: https://github.com/Textualize/textual/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/Textualize/textual/compare/v0.8.2...v0.9.0 [0.8.2]: https://github.com/Textualize/textual/compare/v0.8.1...v0.8.2 [0.8.1]: https://github.com/Textualize/textual/compare/v0.8.0...v0.8.1 From 619ba5f2fdbd7a1f2b447f6d0eb875e9e134325f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 14:51:05 +0000 Subject: [PATCH 05/10] docstring --- src/textual/_win_sleep.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/textual/_win_sleep.py b/src/textual/_win_sleep.py index a542fe62e0..bec60ac7d6 100644 --- a/src/textual/_win_sleep.py +++ b/src/textual/_win_sleep.py @@ -12,8 +12,18 @@ def sleep(sleep_for: float) -> None: + """A replacement sleep for Windows. + + Python 3.11 added a more accurate sleep. This may be used on < Python 3.11 + + Args: + sleep_for (float): Seconds to sleep for. + """ handle = kernel32.CreateWaitableTimerExW( - None, None, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, 0x1F0003 + None, + None, + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + 0x1F0003, ) if not handle: time_sleep(sleep_for) From 97627107cf0bd1c76ab993408df91dde8c3df823 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 15:01:57 +0000 Subject: [PATCH 06/10] add pause --- tests/snapshot_tests/test_snapshots.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index a2c67595ef..5650fcf1c4 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -71,7 +71,8 @@ def test_input_and_focus(snap_compare): "tab", *"Darren", # Focus first input, write "Darren" "tab", - *"Burns", # Tab focus to second input, write "Burns" + *"Burns", + "_", # Tab focus to second input, write "Burns" ] assert snap_compare(WIDGET_EXAMPLES_DIR / "input.py", press=press) From 29e5f764ac3a84419c495d839986692c9464ba26 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 17:00:38 +0000 Subject: [PATCH 07/10] fix for winsleep --- src/textual/_time.py | 1 + src/textual/_win_sleep.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/textual/_time.py b/src/textual/_time.py index d6a8403d16..8b82936d3f 100644 --- a/src/textual/_time.py +++ b/src/textual/_time.py @@ -14,6 +14,7 @@ else: time = monotonic + if WINDOWS: if sys.version_info >= (3, 11, 0): diff --git a/src/textual/_win_sleep.py b/src/textual/_win_sleep.py index bec60ac7d6..88400745bf 100644 --- a/src/textual/_win_sleep.py +++ b/src/textual/_win_sleep.py @@ -31,14 +31,15 @@ def sleep(sleep_for: float) -> None: if not kernel32.SetWaitableTimer( handle, - ctypes.byref(LARGE_INTEGER(int(sleep_for * -10000))), + ctypes.byref(LARGE_INTEGER(int(sleep_for * -10_000_000))), 0, None, None, 0, ): + kernel32.CloseHandle(handle) time_sleep(sleep_for) return kernel32.WaitForSingleObject(handle, INFINITE) - kernel32.CancelWaitableTimer(handle) + kernel32.CloseHandle(handle) From 231fa2125140395632d8eec513afd29805ee9b90 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 17:18:50 +0000 Subject: [PATCH 08/10] fix win sleep --- src/textual/_win_sleep.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/textual/_win_sleep.py b/src/textual/_win_sleep.py index 88400745bf..325411fcab 100644 --- a/src/textual/_win_sleep.py +++ b/src/textual/_win_sleep.py @@ -8,6 +8,7 @@ INFINITE = 0xFFFFFFFF WAIT_FAILED = 0xFFFFFFFF +CREATE_WAITABLE_TIMER_MANUAL_RESET = 0x00000001 CREATE_WAITABLE_TIMER_HIGH_RESOLUTION = 0x00000002 @@ -22,13 +23,14 @@ def sleep(sleep_for: float) -> None: handle = kernel32.CreateWaitableTimerExW( None, None, - CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION | CREATE_WAITABLE_TIMER_MANUAL_RESET, 0x1F0003, ) if not handle: time_sleep(sleep_for) return + sleep_for -= 1 / 1000 if not kernel32.SetWaitableTimer( handle, ctypes.byref(LARGE_INTEGER(int(sleep_for * -10_000_000))), @@ -38,8 +40,10 @@ def sleep(sleep_for: float) -> None: 0, ): kernel32.CloseHandle(handle) + print("error") time_sleep(sleep_for) return - kernel32.WaitForSingleObject(handle, INFINITE) + if kernel32.WaitForSingleObject(handle, INFINITE) == WAIT_FAILED: + time_sleep(sleep_for) kernel32.CloseHandle(handle) From d0529aebd831e5311532c6de05afe034c110ea30 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 17:22:47 +0000 Subject: [PATCH 09/10] no need for manual reset --- src/textual/_win_sleep.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/_win_sleep.py b/src/textual/_win_sleep.py index 325411fcab..e704cef078 100644 --- a/src/textual/_win_sleep.py +++ b/src/textual/_win_sleep.py @@ -8,7 +8,6 @@ INFINITE = 0xFFFFFFFF WAIT_FAILED = 0xFFFFFFFF -CREATE_WAITABLE_TIMER_MANUAL_RESET = 0x00000001 CREATE_WAITABLE_TIMER_HIGH_RESOLUTION = 0x00000002 @@ -23,7 +22,7 @@ def sleep(sleep_for: float) -> None: handle = kernel32.CreateWaitableTimerExW( None, None, - CREATE_WAITABLE_TIMER_HIGH_RESOLUTION | CREATE_WAITABLE_TIMER_MANUAL_RESET, + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, 0x1F0003, ) if not handle: From 91e23ff34c2f34da291c7cadf26dcb5fbde4e439 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 30 Dec 2022 17:31:36 +0000 Subject: [PATCH 10/10] more pauses for demo? --- docs/blog/posts/better-sleep-on-windows.md | 3 +-- tests/snapshot_tests/test_snapshots.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/blog/posts/better-sleep-on-windows.md b/docs/blog/posts/better-sleep-on-windows.md index 351a9ed2dd..65236d8d48 100644 --- a/docs/blog/posts/better-sleep-on-windows.md +++ b/docs/blog/posts/better-sleep-on-windows.md @@ -43,8 +43,7 @@ async def sleep(sleep_for: float) -> None: Args: sleep_for (float): Seconds to sleep for. - """ - print("sleep") + """ await get_running_loop().run_in_executor(None, time_sleep, sleep_for) ``` diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 5650fcf1c4..9eda4b045f 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -179,6 +179,6 @@ def test_demo(snap_compare): """Test the demo app (python -m textual)""" assert snap_compare( Path("../../src/textual/demo.py"), - press=["down", "down", "down", "_"], + press=["down", "down", "down", "_", "_"], terminal_size=(100, 30), )