From 7d92a67729ff1195da6dadc509914adc77f44c04 Mon Sep 17 00:00:00 2001 From: San Date: Mon, 23 Dec 2024 01:02:43 +0900 Subject: [PATCH] Update documentation - Added a section in README.md to clarify the requirement of an existing asyncio event loop for using TSignal's decorators. - Updated the api.md to emphasize the need for a running event loop when using the @t_with_signals and @t_slot decorators. - Clarified that if no event loop is running, a RuntimeError will be raised. Modified the core.py - Logs and raises an error message instead of creating a new event loop when none is found. Update version - Incremented the version number in pyproject.toml from 0.4.2 to 0.4.3. --- README.md | 18 ++++++++++++++++++ docs/api.md | 11 ++++++++--- pyproject.toml | 2 +- src/tsignal/core.py | 7 +++++-- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1a06581..ebca47f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,24 @@ TSignal is a lightweight, pure-Python signal/slot library that provides thread-s - **Weak Reference**: - By setting `weak=True` when connecting a slot, the library holds a weak reference to the receiver object. This allows the receiver to be garbage-collected if there are no other strong references to it. Once garbage-collected, the connection is automatically removed, preventing stale references. +### **Requires an Existing Event Loop** + +Since TSignal relies on Python’s `asyncio` infrastructure for scheduling async slots and cross-thread calls, you **must** have a running event loop before using TSignal’s decorators like `@t_with_signals` or `@t_slot`. Typically, this means: + +1. **Inside `asyncio.run(...)`:** + For example: + ```python + async def main(): + # create objects, do your logic + ... + asyncio.run(main()) + ``` + +2. **@t_with_worker Decorator:** + If you decorate a class with `@t_with_worker`, it automatically creates a worker thread with its own event loop. That pattern is isolated to the worker context, so any other async usage in the main thread also needs its own loop. + +If no event loop is running when a slot is called, TSignal will raise a RuntimeError instead of creating a new loop behind the scenes. This ensures consistent concurrency behavior and avoids hidden loops that might never process tasks. + ## Why TSignal? Modern Python applications often rely on asynchronous operations and multi-threading. Traditional event frameworks either require large external dependencies or lack seamless async/thread support. TSignal provides: diff --git a/docs/api.md b/docs/api.md index 5da18a9..018d129 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,12 +1,14 @@ # API Reference ## Requirements -TSignal requires Python 3.10 or higher. +TSignal requires Python 3.10 or higher, and a running `asyncio` event loop for any async usage. ## Decorators ### `@t_with_signals` Enables signal-slot functionality on a class. Classes decorated with `@t_with_signals` can define signals and have their slots automatically assigned event loops and thread affinity. +**Important**: `@t_with_signals` expects that you already have an `asyncio` event loop running (e.g., via `asyncio.run(...)`) unless you only rely on synchronous slots in a single-thread scenario. When in doubt, wrap your main logic in an async function and call `asyncio.run(main())`. + **Usage:** ```python @t_with_signals @@ -31,7 +33,7 @@ self.my_signal.emit(value) ``` ### `@t_slot` -Marks a method as a slot. Slots can be synchronous or asynchronous methods. Slots automatically handle thread affinity and can be connected to signals. +Marks a method as a slot. Slots can be synchronous or asynchronous methods. TSignal automatically handles cross-thread invocation—**but only if there is a running event loop**. **Usage:** @@ -46,8 +48,11 @@ async def on_async_signal(self, value): print("Async Received:", value) ``` +**Event Loop Requirement**: +If the decorated slot is async, or if the slot might be called from another thread, TSignal uses asyncio scheduling. That means a running event loop is mandatory. If no loop is found, a RuntimeError is raised. + ### `@t_with_worker` -Decorates a class to run inside a dedicated worker thread with its own event loop. Ideal for offloading tasks without blocking the main thread. The worker provides: +Decorates a class to run inside a dedicated worker thread with its own event loop. Ideal for offloading tasks without blocking the main thread. When using @t_with_worker, the worker thread automatically sets up its own event loop, so calls within that worker are safe. For the main thread, you still need an existing loop if you plan on using async slots or cross-thread signals. The worker provides: A dedicated event loop in another thread. The `run(*args, **kwargs)` coroutine as the main entry point. diff --git a/pyproject.toml b/pyproject.toml index 74f114b..80dcfc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tsignal" -version = "0.4.2" +version = "0.4.3" description = "A Python Signal-Slot library inspired by Qt" readme = "README.md" requires-python = ">=3.10" diff --git a/src/tsignal/core.py b/src/tsignal/core.py index 1aab025..7fac8af 100644 --- a/src/tsignal/core.py +++ b/src/tsignal/core.py @@ -801,8 +801,11 @@ async def wrap(self, *args, **kwargs): try: self._tsignal_loop = asyncio.get_running_loop() except RuntimeError: - self._tsignal_loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._tsignal_loop) + t_signal_log_and_raise_error( + logger, + RuntimeError, + "[TSignal][t_slot][wrap] No running event loop found.", + ) if not _tsignal_from_emit.get(): current_thread = threading.current_thread()