diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 7e9c816..11d1462 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v5 diff --git a/async_tkinter_loop/async_tkinter_loop.py b/async_tkinter_loop/async_tkinter_loop.py index 64d6a72..ac628e0 100644 --- a/async_tkinter_loop/async_tkinter_loop.py +++ b/async_tkinter_loop/async_tkinter_loop.py @@ -1,9 +1,9 @@ import _tkinter import asyncio import tkinter as tk -from collections.abc import Coroutine +from collections.abc import Callable, Coroutine from functools import wraps -from typing import Any, Callable +from typing import Any from typing_extensions import ParamSpec @@ -31,22 +31,24 @@ async def main_loop(root: tk.Tk) -> None: def get_event_loop() -> asyncio.AbstractEventLoop: """ - A helper function which returns an event loop using current event loop policy. + A helper function which returns a running event loop. Returns: event loop """ - return asyncio.get_event_loop_policy().get_event_loop() + return asyncio.get_running_loop() -def async_mainloop(root: tk.Tk) -> None: +def async_mainloop(root: tk.Tk, event_loop: asyncio.AbstractEventLoop | None = None) -> None: """ A function, which is a substitute to the standard `root.mainloop()`. Args: root: tkinter root object + event_loop: asyncio event loop (optional) """ - get_event_loop().run_until_complete(main_loop(root)) + event_loop = event_loop or asyncio.new_event_loop() + event_loop.run_until_complete(main_loop(root)) P = ParamSpec("P") @@ -55,6 +57,7 @@ def async_mainloop(root: tk.Tk) -> None: def async_handler( async_function: Callable[P, Coroutine[Any, Any, None]], *args: Any, # noqa: ANN401 + event_loop: asyncio.AbstractEventLoop | None = None, **kwargs: Any, # noqa: ANN401 ) -> Callable[P, None]: """ @@ -64,6 +67,7 @@ def async_handler( Args: async_function: async function args: positional parameters which will be passed to the async function + event_loop: asyncio event loop (optional, for testing purposes) kwargs: keyword parameters which will be passed to the async function Returns: @@ -102,9 +106,10 @@ async def some_async_function(): button = tk.Button("Press me", command=some_async_function) ``` """ + event_loop = event_loop or get_event_loop() @wraps(async_function) def wrapper(*handler_args) -> None: - get_event_loop().create_task(async_function(*handler_args, *args, **kwargs)) + event_loop.create_task(async_function(*handler_args, *args, **kwargs)) return wrapper diff --git a/examples/start_stop_counter.py b/examples/start_stop_counter.py index c8c2ab9..1b888c0 100755 --- a/examples/start_stop_counter.py +++ b/examples/start_stop_counter.py @@ -32,8 +32,7 @@ def start_stop(): event = asyncio.Event() # Start background task -loop = asyncio.new_event_loop() -asyncio.set_event_loop(loop) -task = loop.create_task(counter()) +event_loop = asyncio.new_event_loop() +task = event_loop.create_task(counter()) -async_mainloop(root) +async_mainloop(root, event_loop) diff --git a/poetry.lock b/poetry.lock index d3be350..a3fc562 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "anyio" @@ -376,7 +376,7 @@ files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] -markers = {main = "extra == \"examples\" and python_version < \"3.11\"", dev = "python_version < \"3.11\""} +markers = {main = "extra == \"examples\" and python_version == \"3.10\"", dev = "python_version == \"3.10\""} [package.extras] test = ["pytest (>=6)"] @@ -492,31 +492,6 @@ markers = {main = "extra == \"examples\""} [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -559,9 +534,6 @@ files = [ {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, ] -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - [package.extras] docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] @@ -665,7 +637,6 @@ files = [ click = ">=7.0" colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" markdown = ">=3.3.6" markupsafe = ">=2.0.1" @@ -711,7 +682,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} mergedeep = ">=1.3.4" platformdirs = ">=2.2.0" pyyaml = ">=5.1" @@ -771,7 +741,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} Jinja2 = ">=2.11.1" Markdown = ">=3.6" MarkupSafe = ">=1.1" @@ -1380,31 +1349,10 @@ files = [ [package.extras] watchmedo = ["PyYAML (>=3.10)"] -[[package]] -name = "zipp" -version = "3.21.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [extras] examples = ["Pillow", "customtkinter", "httpx"] [metadata] lock-version = "2.1" -python-versions = "^3.9" -content-hash = "f7ee63e7b6a43abd3ae51983a95579f0738d3721811ca34f3105f4a768e412ad" +python-versions = "^3.10" +content-hash = "0823bfbc1ae0a4332318f742467dcd8a208f4bfdf955a916694634f0b8f4a189" diff --git a/pyproject.toml b/pyproject.toml index 2141c77..94c4c0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" Pillow = {version = ">=10.3.0,<12.0.0", optional = true} httpx = {version = ">=0.23.1,<0.29.0", optional = true} customtkinter = {version = "^5.2.1", optional = true} @@ -44,7 +44,7 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 120 [tool.ruff.lint] diff --git a/tests/test_async_tk_loop.py b/tests/test_async_tk_loop.py index 16f3022..975291b 100644 --- a/tests/test_async_tk_loop.py +++ b/tests/test_async_tk_loop.py @@ -9,13 +9,6 @@ TIMEOUT = 60 -@pytest.mark.timeout(TIMEOUT) -def test_destroy(): - root = Tk() - root.destroy() - async_mainloop(root) - - @pytest.mark.timeout(TIMEOUT) def test_async_command(): root = Tk() @@ -25,9 +18,10 @@ async def button_pressed(): await asyncio.sleep(0.1) root.destroy() - async_handler(button_pressed)() + event_loop = asyncio.new_event_loop() + async_handler(button_pressed, event_loop=event_loop)() - async_mainloop(root) + async_mainloop(root, event_loop) @pytest.mark.timeout(TIMEOUT) @@ -38,9 +32,10 @@ async def on_click(_event): await asyncio.sleep(0.1) root.destroy() - async_handler(on_click)(Mock("Event")) + event_loop = asyncio.new_event_loop() + async_handler(on_click, event_loop=event_loop)(Mock("Event")) - async_mainloop(root) + async_mainloop(root, event_loop) @pytest.mark.timeout(TIMEOUT) @@ -48,25 +43,31 @@ def test_async_command_as_decorator(): root = Tk() # Simulate a click on a button which closes the window with some delay - @async_handler + # @async_handler async def button_pressed(): await asyncio.sleep(0.1) root.destroy() + event_loop = asyncio.new_event_loop() + button_pressed = async_handler(button_pressed, event_loop=event_loop) button_pressed() - async_mainloop(root) + async_mainloop(root, event_loop) @pytest.mark.timeout(TIMEOUT) def test_async_event_handler_as_decorator(): root = Tk() - @async_handler + event_loop = asyncio.new_event_loop() + + # @async_handler async def on_click(_event): await asyncio.sleep(0.1) root.destroy() + event_loop = asyncio.new_event_loop() + on_click = async_handler(on_click, event_loop=event_loop) on_click(Mock("Event")) - async_mainloop(root) + async_mainloop(root, event_loop)