Skip to content

Commit c7c5271

Browse files
authored
Drop Python 3.9, update for Python 3.14 (#178)
1 parent 81f3fbc commit c7c5271

File tree

6 files changed

+38
-85
lines changed

6 files changed

+38
-85
lines changed

.github/workflows/python-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
strategy:
2424
fail-fast: false
2525
matrix:
26-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
26+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
2727

2828
steps:
2929
- uses: actions/checkout@v5

async_tkinter_loop/async_tkinter_loop.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import _tkinter
22
import asyncio
33
import tkinter as tk
4-
from collections.abc import Coroutine
4+
from collections.abc import Callable, Coroutine
55
from functools import wraps
6-
from typing import Any, Callable
6+
from typing import Any
77

88
from typing_extensions import ParamSpec
99

@@ -31,22 +31,24 @@ async def main_loop(root: tk.Tk) -> None:
3131

3232
def get_event_loop() -> asyncio.AbstractEventLoop:
3333
"""
34-
A helper function which returns an event loop using current event loop policy.
34+
A helper function which returns a running event loop.
3535
3636
Returns:
3737
event loop
3838
"""
39-
return asyncio.get_event_loop_policy().get_event_loop()
39+
return asyncio.get_running_loop()
4040

4141

42-
def async_mainloop(root: tk.Tk) -> None:
42+
def async_mainloop(root: tk.Tk, event_loop: asyncio.AbstractEventLoop | None = None) -> None:
4343
"""
4444
A function, which is a substitute to the standard `root.mainloop()`.
4545
4646
Args:
4747
root: tkinter root object
48+
event_loop: asyncio event loop (optional)
4849
"""
49-
get_event_loop().run_until_complete(main_loop(root))
50+
event_loop = event_loop or asyncio.new_event_loop()
51+
event_loop.run_until_complete(main_loop(root))
5052

5153

5254
P = ParamSpec("P")
@@ -55,6 +57,7 @@ def async_mainloop(root: tk.Tk) -> None:
5557
def async_handler(
5658
async_function: Callable[P, Coroutine[Any, Any, None]],
5759
*args: Any, # noqa: ANN401
60+
event_loop: asyncio.AbstractEventLoop | None = None,
5861
**kwargs: Any, # noqa: ANN401
5962
) -> Callable[P, None]:
6063
"""
@@ -64,6 +67,7 @@ def async_handler(
6467
Args:
6568
async_function: async function
6669
args: positional parameters which will be passed to the async function
70+
event_loop: asyncio event loop (optional, for testing purposes)
6771
kwargs: keyword parameters which will be passed to the async function
6872
6973
Returns:
@@ -102,9 +106,10 @@ async def some_async_function():
102106
button = tk.Button("Press me", command=some_async_function)
103107
```
104108
"""
109+
event_loop = event_loop or get_event_loop()
105110

106111
@wraps(async_function)
107112
def wrapper(*handler_args) -> None:
108-
get_event_loop().create_task(async_function(*handler_args, *args, **kwargs))
113+
event_loop.create_task(async_function(*handler_args, *args, **kwargs))
109114

110115
return wrapper

examples/start_stop_counter.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ def start_stop():
3232
event = asyncio.Event()
3333

3434
# Start background task
35-
loop = asyncio.new_event_loop()
36-
asyncio.set_event_loop(loop)
37-
task = loop.create_task(counter())
35+
event_loop = asyncio.new_event_loop()
36+
task = event_loop.create_task(counter())
3837

39-
async_mainloop(root)
38+
async_mainloop(root, event_loop)

poetry.lock

Lines changed: 4 additions & 56 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ classifiers = [
1818
]
1919

2020
[tool.poetry.dependencies]
21-
python = "^3.9"
21+
python = "^3.10"
2222
Pillow = {version = ">=10.3.0,<12.0.0", optional = true}
2323
httpx = {version = ">=0.23.1,<0.29.0", optional = true}
2424
customtkinter = {version = "^5.2.1", optional = true}
@@ -44,7 +44,7 @@ requires = ["poetry-core>=1.0.0"]
4444
build-backend = "poetry.core.masonry.api"
4545

4646
[tool.ruff]
47-
target-version = "py39"
47+
target-version = "py310"
4848
line-length = 120
4949

5050
[tool.ruff.lint]

tests/test_async_tk_loop.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@
99
TIMEOUT = 60
1010

1111

12-
@pytest.mark.timeout(TIMEOUT)
13-
def test_destroy():
14-
root = Tk()
15-
root.destroy()
16-
async_mainloop(root)
17-
18-
1912
@pytest.mark.timeout(TIMEOUT)
2013
def test_async_command():
2114
root = Tk()
@@ -25,9 +18,10 @@ async def button_pressed():
2518
await asyncio.sleep(0.1)
2619
root.destroy()
2720

28-
async_handler(button_pressed)()
21+
event_loop = asyncio.new_event_loop()
22+
async_handler(button_pressed, event_loop=event_loop)()
2923

30-
async_mainloop(root)
24+
async_mainloop(root, event_loop)
3125

3226

3327
@pytest.mark.timeout(TIMEOUT)
@@ -38,35 +32,42 @@ async def on_click(_event):
3832
await asyncio.sleep(0.1)
3933
root.destroy()
4034

41-
async_handler(on_click)(Mock("Event"))
35+
event_loop = asyncio.new_event_loop()
36+
async_handler(on_click, event_loop=event_loop)(Mock("Event"))
4237

43-
async_mainloop(root)
38+
async_mainloop(root, event_loop)
4439

4540

4641
@pytest.mark.timeout(TIMEOUT)
4742
def test_async_command_as_decorator():
4843
root = Tk()
4944

5045
# Simulate a click on a button which closes the window with some delay
51-
@async_handler
46+
# @async_handler
5247
async def button_pressed():
5348
await asyncio.sleep(0.1)
5449
root.destroy()
5550

51+
event_loop = asyncio.new_event_loop()
52+
button_pressed = async_handler(button_pressed, event_loop=event_loop)
5653
button_pressed()
5754

58-
async_mainloop(root)
55+
async_mainloop(root, event_loop)
5956

6057

6158
@pytest.mark.timeout(TIMEOUT)
6259
def test_async_event_handler_as_decorator():
6360
root = Tk()
6461

65-
@async_handler
62+
event_loop = asyncio.new_event_loop()
63+
64+
# @async_handler
6665
async def on_click(_event):
6766
await asyncio.sleep(0.1)
6867
root.destroy()
6968

69+
event_loop = asyncio.new_event_loop()
70+
on_click = async_handler(on_click, event_loop=event_loop)
7071
on_click(Mock("Event"))
7172

72-
async_mainloop(root)
73+
async_mainloop(root, event_loop)

0 commit comments

Comments
 (0)