Skip to content

Commit 525a209

Browse files
committed
feat: Add worker thread pattern and enhance signal connections
This commit introduces several major enhancements: 1. Worker Thread Pattern - Add @t_with_worker decorator for background thread management - Implement task queue with async support - Add graceful initialization/cleanup lifecycle - Add comprehensive worker thread tests 2. Signal Connection Enhancement - Support direct function/lambda connections - Support method connections without @t_slot - Improve thread-safe signal emission - Clean up debug logging 3. Documentation - Add worker pattern documentation and examples - Update API reference for new connection types - Add Windows-specific IOCP termination notes - Reorganize examples for better clarity 4. Example Reorganization - Rename examples for consistency - Add worker thread pattern example - Update thread communication examples Test Coverage: 100% Breaking Changes: None
1 parent 366de23 commit 525a209

19 files changed

+1148
-121
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Automatic connection type detection
1515
- Comprehensive test suite
1616
- Full documentation
17+
18+
## [0.1.1] - 2024-12-01
19+
20+
### Changed
21+
- Refactored signal connection logic to support direct function connections.
22+
- Improved error handling for invalid connections.
23+
- Enhanced logging for signal emissions and connections.
24+
25+
### Fixed
26+
- Resolved issues with disconnecting slots during signal emissions.
27+
- Fixed bugs related to async slot processing and connection management.
28+
29+
### Removed
30+
- Deprecated unused constants and methods from the core module.

README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,88 @@ Please see [Contributing Guidelines](CONTRIBUTING.md) for details on how to cont
132132

133133
## License
134134
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
135+
136+
## Connecting Signals and Slots
137+
138+
### Classic Object-Member Connection
139+
```python
140+
@t_with_signals
141+
class Counter:
142+
@t_signal
143+
def count_changed(self):
144+
pass
145+
146+
@t_with_signals
147+
class Display:
148+
@t_slot
149+
def on_count_changed(self, value):
150+
print(f"Count is now: {value}")
151+
152+
counter = Counter()
153+
display = Display()
154+
counter.count_changed.connect(display, display.on_count_changed)
155+
```
156+
157+
### Function Connection
158+
```python
159+
# Connect to a simple function
160+
def print_value(value):
161+
print(f"Value: {value}")
162+
163+
counter.count_changed.connect(print_value)
164+
165+
# Connect to a lambda
166+
counter.count_changed.connect(lambda x: print(f"Lambda received: {x}"))
167+
168+
# Connect to an object method without @t_slot
169+
class Handler:
170+
def process_value(self, value):
171+
print(f"Processing: {value}")
172+
173+
handler = Handler()
174+
counter.count_changed.connect(handler.process_value)
175+
```
176+
177+
## Worker Thread Pattern
178+
179+
TSignal provides a worker thread pattern that combines thread management with signal/slot communication and task queuing:
180+
181+
```python
182+
from tsignal import t_with_worker
183+
184+
@t_with_worker
185+
class DataProcessor:
186+
async def initialize(self, config=None):
187+
# Setup worker (called in worker thread)
188+
self.config = config or {}
189+
190+
async def process_data(self, data):
191+
# Heavy processing in worker thread
192+
result = await heavy_computation(data)
193+
self.processing_done.emit(result)
194+
195+
async def finalize(self):
196+
# Cleanup worker (called before thread stops)
197+
await self.cleanup()
198+
199+
@t_signal
200+
def processing_done(self):
201+
pass
202+
203+
# Usage
204+
processor = DataProcessor()
205+
processor.start(config={'threads': 4}) # Starts worker thread
206+
207+
# Queue task in worker thread
208+
await processor.queue_task(processor.process_data(some_data))
209+
210+
# Stop worker
211+
processor.stop() # Graceful shutdown
212+
```
213+
214+
The worker pattern provides:
215+
- Dedicated worker thread with event loop
216+
- Built-in signal/slot support
217+
- Async task queue
218+
- Graceful initialization/shutdown
219+
- Thread-safe communication

docs/api.md

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,61 @@ async def on_async_signal(self, *args, **kwargs):
3636
pass
3737
```
3838

39+
### `@t_with_worker`
40+
Class decorator that creates a worker thread with signal support and task queue.
41+
42+
**Requirements:**
43+
- Class must implement async `initialize(self, *args, **kwargs)` method
44+
- Class must implement async `finalize(self)` method
45+
46+
**Added Methods:**
47+
##### `start(*args, **kwargs) -> None`
48+
Starts the worker thread and calls initialize with given arguments.
49+
50+
##### `stop() -> None`
51+
Stops the worker thread gracefully, calling finalize.
52+
53+
##### `async queue_task(coro) -> None`
54+
Queues a coroutine for execution in the worker thread.
55+
56+
**Example:**
57+
```python
58+
@t_with_worker
59+
class Worker:
60+
async def initialize(self):
61+
print("Worker initialized")
62+
63+
async def finalize(self):
64+
print("Worker cleanup")
65+
66+
async def process(self):
67+
await asyncio.sleep(1)
68+
print("Processing done")
69+
70+
worker = Worker()
71+
worker.start()
72+
await worker.queue_task(worker.process())
73+
worker.stop()
74+
```
75+
3976
## Classes
4077

4178
### `TSignal`
4279
Base class for signals.
4380

4481
#### Methods
4582

46-
##### `connect(receiver: object, slot: Callable, connection_type: Optional[TConnectionType] = None) -> None`
83+
##### `connect(receiver_or_slot: Union[object, Callable], slot: Optional[Callable] = None) -> None`
4784
Connects the signal to a slot.
4885

4986
**Parameters:**
50-
- `receiver`: Object that contains the slot
51-
- `slot`: Callable that will receive the signal
52-
- `connection_type`: Optional connection type (DirectConnection or QueuedConnection)
87+
- When connecting to a QObject slot:
88+
- `receiver_or_slot`: The receiver object
89+
- `slot`: The slot method of the receiver
90+
91+
- When connecting to a function/lambda:
92+
- `receiver_or_slot`: The callable (function, lambda, or method)
93+
- `slot`: None
5394

5495
##### `disconnect(receiver: Optional[object] = None, slot: Optional[Callable] = None) -> int`
5596
Disconnects one or more slots from the signal.
@@ -107,16 +148,6 @@ Enum defining connection types.
107148
- `DirectConnection`: Slot is called directly in the emitting thread
108149
- `QueuedConnection`: Slot is queued in the receiver's event loop
109150

110-
## Constants
111-
112-
### `TSignalConstants`
113-
Constants used by the TSignal system.
114-
115-
#### Values:
116-
- `FROM_EMIT`: Key for emission context
117-
- `THREAD`: Key for thread storage
118-
- `LOOP`: Key for event loop storage
119-
120151
## Usage Examples
121152

122153
### Basic Signal-Slot

docs/usage.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
4. [Connection Types](#connection-types)
88
5. [Threading and Async](#threading-and-async)
99
6. [Best Practices](#best-practices)
10+
7. [Worker Thread Pattern](#worker-thread-pattern)
1011

1112
## Basic Concepts
1213
TSignal implements the signal-slot pattern, which allows for loose coupling between components. The core concepts are:
@@ -277,3 +278,140 @@ This understanding of signal disconnection behavior is crucial for:
277278
- Ensuring proper resource cleanup
278279
- Managing complex async operations
279280
- Handling thread synchronization correctly
281+
282+
## Signal Connection Types
283+
284+
### Object-Member Connection
285+
Traditional signal-slot connection between objects:
286+
```python
287+
@t_with_signals
288+
class Sender:
289+
@t_signal
290+
def value_changed(self):
291+
pass
292+
293+
@t_with_signals
294+
class Receiver:
295+
@t_slot
296+
def on_value_changed(self, value):
297+
print(f"Value: {value}")
298+
299+
sender.value_changed.connect(receiver, receiver.on_value_changed)
300+
```
301+
302+
### Function Connection
303+
Connect signals directly to functions or lambdas:
304+
```python
305+
# Standalone function
306+
def handle_value(value):
307+
print(f"Value: {value}")
308+
sender.value_changed.connect(handle_value)
309+
310+
# Lambda function
311+
sender.value_changed.connect(lambda x: print(f"Value: {x}"))
312+
```
313+
314+
### Method Connection
315+
Connect to object methods without @t_slot decorator:
316+
```python
317+
class Handler:
318+
def process(self, value):
319+
print(f"Processing: {value}")
320+
321+
handler = Handler()
322+
sender.value_changed.connect(handler.process)
323+
```
324+
325+
### Connection Behavior Notes
326+
- Object-member connections are automatically disconnected when the receiver is destroyed
327+
- Function connections remain active until explicitly disconnected
328+
- Method connections behave like function connections and need manual cleanup
329+
330+
## Worker Thread Pattern
331+
332+
### Overview
333+
The worker pattern provides a convenient way to run operations in a background thread with built-in signal/slot support and task queuing.
334+
335+
### Basic Worker
336+
```python
337+
@t_with_worker
338+
class ImageProcessor:
339+
async def initialize(self, cache_size=100):
340+
"""Called when worker starts"""
341+
self.cache = {}
342+
self.cache_size = cache_size
343+
344+
async def finalize(self):
345+
"""Called before worker stops"""
346+
self.cache.clear()
347+
348+
@t_signal
349+
def processing_complete(self):
350+
"""Signal emitted when processing is done"""
351+
pass
352+
353+
async def process_image(self, image_data):
354+
"""Task to be executed in worker thread"""
355+
result = await self.heavy_processing(image_data)
356+
self.processing_complete.emit(result)
357+
358+
# Usage
359+
processor = ImageProcessor()
360+
processor.start(cache_size=200)
361+
362+
# Queue tasks
363+
await processor.queue_task(processor.process_image(data1))
364+
await processor.queue_task(processor.process_image(data2))
365+
366+
# Cleanup
367+
processor.stop()
368+
```
369+
370+
### Worker Features
371+
372+
#### Thread Safety
373+
- All signal emissions are thread-safe
374+
- Task queue is thread-safe
375+
- Worker has its own event loop
376+
377+
#### Task Queue
378+
- Tasks are executed sequentially
379+
- Tasks must be coroutines
380+
- Queue is processed in worker thread
381+
382+
#### Lifecycle Management
383+
```python
384+
@t_with_worker
385+
class Worker:
386+
async def initialize(self, *args, **kwargs):
387+
# Setup code
388+
self.resources = await setup_resources()
389+
390+
async def finalize(self):
391+
# Cleanup code
392+
await self.resources.cleanup()
393+
394+
async def some_task(self):
395+
# Will run in worker thread
396+
await self.resources.process()
397+
398+
worker = Worker()
399+
try:
400+
worker.start()
401+
await worker.queue_task(worker.some_task())
402+
finally:
403+
worker.stop() # Ensures cleanup
404+
```
405+
406+
#### Combining with Signals
407+
Workers can use signals to communicate results:
408+
```python
409+
@t_with_worker
410+
class DataProcessor:
411+
def __init__(self):
412+
super().__init__()
413+
self.results = []
414+
415+
@t_signal
416+
def processing_done(self):
417+
"""Emitted when batch is

0 commit comments

Comments
 (0)