or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

async-decorators.mdevent-loop.mdindex.mdthread-executor.mdutilities.md

async-decorators.mddocs/

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

```