Skip to content

Commit

Permalink
Merge pull request #11 from TSignalDev/0.4.3
Browse files Browse the repository at this point in the history
Update documentation
  • Loading branch information
san-tekart authored Dec 22, 2024
2 parents 5cd6c27 + 7d92a67 commit 4b72210
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 6 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:**

Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 5 additions & 2 deletions src/tsignal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 4b72210

Please sign in to comment.