0
# Async Decorators
1
2
Decorators that enable Qt slots and event handlers to run as async coroutines, providing seamless integration between asyncio's async/await syntax and Qt's signal-slot system.
3
4
## Capabilities
5
6
### Async Slot Decorator
7
8
Converts Qt slots to run asynchronously on the asyncio event loop, allowing signal handlers to use async/await syntax without blocking the UI thread.
9
10
```python { .api }
11
def asyncSlot(*args, **kwargs):
12
"""
13
Make a Qt slot run asynchronously on the asyncio loop.
14
15
This decorator allows Qt slots to be defined as async functions that will
16
be executed as coroutines when the signal is emitted.
17
18
Args:
19
*args: Signal parameter types (same as Qt's Slot decorator)
20
**kwargs: Additional keyword arguments for Qt's Slot decorator
21
22
Returns:
23
Decorator function that wraps the async slot
24
25
Raises:
26
TypeError: If slot signature doesn't match signal parameters
27
"""
28
```
29
30
#### Usage Example
31
32
```python
33
import asyncio
34
import sys
35
from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel
36
from qasync import QEventLoop, asyncSlot
37
38
class AsyncWidget(QWidget):
39
def __init__(self):
40
super().__init__()
41
self.setup_ui()
42
43
def setup_ui(self):
44
layout = QVBoxLayout()
45
46
self.status_label = QLabel("Ready")
47
self.button = QPushButton("Start Async Task")
48
49
# Connect button to async slot
50
self.button.clicked.connect(self.handle_button_click)
51
52
layout.addWidget(self.status_label)
53
layout.addWidget(self.button)
54
self.setLayout(layout)
55
56
@asyncSlot()
57
async def handle_button_click(self):
58
"""Handle button click asynchronously."""
59
self.status_label.setText("Processing...")
60
self.button.setEnabled(False)
61
62
try:
63
# Simulate async work (network request, file I/O, etc.)
64
await asyncio.sleep(2)
65
result = await self.fetch_data()
66
67
self.status_label.setText(f"Complete: {result}")
68
except Exception as e:
69
self.status_label.setText(f"Error: {e}")
70
finally:
71
self.button.setEnabled(True)
72
73
async def fetch_data(self):
74
# Simulate async data fetching
75
await asyncio.sleep(1)
76
return "Data loaded successfully"
77
78
if __name__ == "__main__":
79
app = QApplication(sys.argv)
80
widget = AsyncWidget()
81
widget.show()
82
83
app_close_event = asyncio.Event()
84
app.aboutToQuit.connect(app_close_event.set)
85
86
asyncio.run(app_close_event.wait(), loop_factory=QEventLoop)
87
```
88
89
### Signal Parameter Handling
90
91
The decorator automatically handles signal parameter matching, removing excess parameters if the slot signature doesn't match the signal.
92
93
#### Usage Example
94
95
```python
96
from PySide6.QtCore import QTimer, pyqtSignal
97
from qasync import asyncSlot
98
99
class TimerWidget(QWidget):
100
# Custom signal with parameters
101
data_received = pyqtSignal(str, int)
102
103
def __init__(self):
104
super().__init__()
105
106
# Connect signals with different parameter counts
107
self.data_received.connect(self.handle_data_full)
108
self.data_received.connect(self.handle_data_partial)
109
110
# Timer signal (no parameters)
111
timer = QTimer()
112
timer.timeout.connect(self.handle_timeout)
113
timer.start(1000)
114
115
@asyncSlot(str, int) # Matches signal parameters exactly
116
async def handle_data_full(self, message, value):
117
print(f"Full data: {message}, {value}")
118
await asyncio.sleep(0.1)
119
120
@asyncSlot(str) # Only uses first parameter
121
async def handle_data_partial(self, message):
122
print(f"Partial data: {message}")
123
await asyncio.sleep(0.1)
124
125
@asyncSlot() # No parameters
126
async def handle_timeout(self):
127
print("Timer tick")
128
await asyncio.sleep(0.1)
129
```
130
131
### Async Close Handler
132
133
Decorator for enabling async cleanup operations before application or widget closure.
134
135
```python { .api }
136
def asyncClose(fn):
137
"""
138
Allow async code to run before application or widget closure.
139
140
This decorator wraps close event handlers to allow async operations
141
during the shutdown process.
142
143
Args:
144
fn: Async function to run during close event
145
146
Returns:
147
Wrapped function that handles the close event
148
"""
149
```
150
151
#### Usage Example
152
153
```python
154
import asyncio
155
from PySide6.QtWidgets import QMainWindow, QApplication
156
from qasync import QEventLoop, asyncClose
157
158
class MainWindow(QMainWindow):
159
def __init__(self):
160
super().__init__()
161
self.setWindowTitle("Async Close Example")
162
self.active_connections = []
163
164
async def cleanup_connections(self):
165
"""Clean up active network connections."""
166
print("Closing connections...")
167
for connection in self.active_connections:
168
await connection.close()
169
print("All connections closed")
170
171
async def save_user_data(self):
172
"""Save user data asynchronously."""
173
print("Saving user data...")
174
await asyncio.sleep(1) # Simulate async save operation
175
print("User data saved")
176
177
@asyncClose
178
async def closeEvent(self, event):
179
"""Handle window close event asynchronously."""
180
print("Application closing...")
181
182
# Perform async cleanup operations
183
await self.cleanup_connections()
184
await self.save_user_data()
185
186
print("Cleanup complete")
187
# Event is automatically accepted after async operations complete
188
189
if __name__ == "__main__":
190
app = QApplication(sys.argv)
191
window = MainWindow()
192
window.show()
193
194
app_close_event = asyncio.Event()
195
app.aboutToQuit.connect(app_close_event.set)
196
197
asyncio.run(app_close_event.wait(), loop_factory=QEventLoop)
198
```
199
200
## Error Handling
201
202
Both decorators provide proper error handling for async operations:
203
204
### Async Slot Error Handling
205
206
Exceptions in async slots are handled through Python's standard exception handling mechanism:
207
208
```python
209
import sys
210
from qasync import asyncSlot
211
212
class ErrorHandlingWidget(QWidget):
213
@asyncSlot()
214
async def risky_operation(self):
215
try:
216
await self.might_fail()
217
except Exception as e:
218
print(f"Slot error: {e}")
219
# Handle error appropriately
220
self.show_error_message(str(e))
221
222
async def might_fail(self):
223
# This might raise an exception
224
raise ValueError("Something went wrong!")
225
226
def show_error_message(self, message):
227
# Show error to user
228
from PySide6.QtWidgets import QMessageBox
229
QMessageBox.critical(self, "Error", message)
230
```
231
232
### Exception Propagation
233
234
Unhandled exceptions in async slots are propagated through the standard Python exception handling system (sys.excepthook) and can be logged or handled globally:
235
236
```python
237
import sys
238
import logging
239
240
# Set up global exception handler
241
logging.basicConfig(level=logging.ERROR)
242
logger = logging.getLogger(__name__)
243
244
def handle_exception(exc_type, exc_value, exc_traceback):
245
if issubclass(exc_type, KeyboardInterrupt):
246
sys.__excepthook__(exc_type, exc_value, exc_traceback)
247
return
248
249
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
250
251
sys.excepthook = handle_exception
252
```
253
254
## Advanced Usage Patterns
255
256
### Connecting Multiple Signals
257
258
```python
259
class MultiSignalWidget(QWidget):
260
def __init__(self):
261
super().__init__()
262
263
button1 = QPushButton("Button 1")
264
button2 = QPushButton("Button 2")
265
266
# Same async slot handles multiple signals
267
button1.clicked.connect(self.handle_click)
268
button2.clicked.connect(self.handle_click)
269
270
@asyncSlot()
271
async def handle_click(self):
272
sender = self.sender()
273
print(f"Async click from: {sender.text()}")
274
await asyncio.sleep(0.5)
275
```
276
277
### Chaining Async Operations
278
279
```python
280
class ChainedOperationsWidget(QWidget):
281
@asyncSlot()
282
async def start_workflow(self):
283
"""Chain multiple async operations."""
284
try:
285
step1_result = await self.step_one()
286
step2_result = await self.step_two(step1_result)
287
final_result = await self.step_three(step2_result)
288
289
self.display_result(final_result)
290
except Exception as e:
291
self.handle_workflow_error(e)
292
293
async def step_one(self):
294
await asyncio.sleep(1)
295
return "Step 1 complete"
296
297
async def step_two(self, input_data):
298
await asyncio.sleep(1)
299
return f"{input_data} -> Step 2 complete"
300
301
async def step_three(self, input_data):
302
await asyncio.sleep(1)
303
return f"{input_data} -> Step 3 complete"
304
```
305
306
### Task Cancellation
307
308
```python
309
class CancellableTaskWidget(QWidget):
310
def __init__(self):
311
super().__init__()
312
self.current_task = None
313
314
self.start_button = QPushButton("Start")
315
self.cancel_button = QPushButton("Cancel")
316
317
self.start_button.clicked.connect(self.start_long_task)
318
self.cancel_button.clicked.connect(self.cancel_task)
319
320
@asyncSlot()
321
async def start_long_task(self):
322
if self.current_task and not self.current_task.done():
323
return # Task already running
324
325
self.current_task = asyncio.create_task(self.long_running_operation())
326
327
try:
328
result = await self.current_task
329
print(f"Task completed: {result}")
330
except asyncio.CancelledError:
331
print("Task was cancelled")
332
333
@asyncSlot()
334
async def cancel_task(self):
335
if self.current_task and not self.current_task.done():
336
self.current_task.cancel()
337
338
async def long_running_operation(self):
339
for i in range(10):
340
await asyncio.sleep(1)
341
print(f"Progress: {i + 1}/10")
342
return "Long task complete"
343
```