Coverage for signals/dispatch/dispatcher.py: 70%

217 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-16 09:17 +0330

1import asyncio 

2import logging 

3import threading 

4import weakref 

5from collections.abc import Awaitable, Hashable, Iterator 

6from types import FunctionType, MethodType 

7from typing import Any, Protocol, Callable, cast, overload 

8 

9from asgiref.sync import async_to_sync, iscoroutinefunction, sync_to_async 

10 

11from lazy_settings.conf import settings 

12 

13from signals import global_settings 

14from signals.utils.inspect import func_accepts_kwargs 

15 

16 

17logger = logging.getLogger("signals.dispatch") 

18 

19settings.register(global_settings) 

20 

21 

22@overload 

23def _make_id(target: MethodType) -> tuple[int, int]: ... 

24 

25 

26@overload 

27def _make_id(target: type | FunctionType | None | Any) -> int: ... 

28 

29 

30def _make_id(target: None | Any) -> tuple[int, int] | int: 

31 if target and hasattr(target, "__func__"): 

32 return id(target.__self__), id(target.__func__) 

33 return id(target) 

34 

35 

36NONE_ID = _make_id(None) 

37 

38# A marker for caching 

39NO_RECEIVERS = 1 

40 

41 

42class Receiver(Protocol): 

43 def __call__( 

44 self, *, signal: "Signal", sender: object, **kwargs: Any 

45 ) -> Any | Awaitable[Any]: ... 

46 

47 __qualname__: str 

48 

49 

50type RECEIVER_T = Receiver 

51type RECEIVER_REF_T = weakref.ReferenceType[RECEIVER_T] 

52type LOOKUP_KEY_T = tuple[Hashable | tuple[int, int] | int, tuple[int, int] | int] 

53 

54 

55class Signal: 

56 """ 

57 Base class for all signals 

58 

59 Internal attributes: 

60 

61 receivers: 

62 [((id(receiver), id(sender)), ref(receiver), ref(sender), is_async)] 

63 sender_receivers_cache: 

64 WeakKeyDictionary[sender, list[receiver]] 

65 """ 

66 

67 def __init__(self, use_caching: bool = False) -> None: 

68 """ 

69 Create a new signal. 

70 """ 

71 self.receivers: list[ 

72 tuple[ 

73 tuple[Hashable | tuple[int, int] | int, tuple[int, int] | int], 

74 RECEIVER_T | RECEIVER_REF_T, 

75 Any, 

76 bool, 

77 ] 

78 ] = [] 

79 self.lock = threading.Lock() 

80 self.use_caching = use_caching 

81 # For convenience we create empty caches even if they are not used. 

82 # A note about caching: if use_caching is defined, then for each 

83 # distinct sender we cache the receivers that sender has in 

84 # 'sender_receivers_cache'. The cache is cleaned when .connect() or 

85 # .disconnect() is called and populated on send(). 

86 self.sender_receivers_cache: ( 

87 weakref.WeakKeyDictionary[ 

88 Any, 

89 list[tuple[RECEIVER_T | RECEIVER_REF_T, tuple[int, int] | int, bool]] 

90 | int, 

91 ] 

92 | dict[ 

93 Any, 

94 list[tuple[RECEIVER_T | RECEIVER_REF_T, tuple[int, int] | int, bool]] 

95 | int, 

96 ] 

97 ) = weakref.WeakKeyDictionary() if use_caching else {} 

98 self._dead_receivers: bool = False 

99 

100 def connect( 

101 self, 

102 receiver: RECEIVER_T, 

103 sender: Any | None = None, 

104 weak: bool = True, 

105 dispatch_uid: Hashable | None = None, 

106 ) -> None: 

107 """ 

108 Connect receiver to sender for signal. 

109 

110 Arguments: 

111 

112 receiver 

113 A function or an instance method which is to receive signals. 

114 Receivers must be hashable objects. Receivers can be 

115 asynchronous. 

116 

117 If weak is True, then receiver must be weak referenceable. 

118 

119 Receivers must be able to accept keyword arguments. 

120 

121 If a receiver is connected with a dispatch_uid argument, it 

122 will not be added if another receiver was already connected 

123 with that dispatch_uid. 

124 

125 sender 

126 The sender to which the receiver should respond. Must either be 

127 a Python object, or None to receive events from any sender. 

128 

129 weak 

130 Whether to use weak references to the receiver. By default, the 

131 module will attempt to use weak references to the receiver 

132 objects. If this parameter is false, then strong references will 

133 be used. 

134 

135 dispatch_uid 

136 An identifier used to uniquely identify a particular instance of 

137 a receiver. This will usually be a string, though it may be 

138 anything hashable. 

139 """ 

140 

141 # If DEBUG is on, check that we got a good receiver 

142 if settings.configured and settings.DEBUG: 

143 if not callable(receiver): 

144 raise TypeError("Signal receivers must be callable.") 

145 # Check for **kwargs 

146 if not func_accepts_kwargs(receiver): 

147 raise ValueError( 

148 "Signal receivers must accept keyword arguments (**kwargs)." 

149 ) 

150 

151 lookup_key: LOOKUP_KEY_T 

152 if dispatch_uid: 

153 lookup_key = ( 

154 dispatch_uid, 

155 _make_id(sender), 

156 ) 

157 else: 

158 lookup_key = (_make_id(receiver), _make_id(sender)) 

159 

160 is_async: bool = iscoroutinefunction(receiver) 

161 

162 receiver_to_store: RECEIVER_T | RECEIVER_REF_T 

163 

164 if weak: 

165 ref: ( 

166 type[weakref.ref[RECEIVER_T]] | type[weakref.WeakMethod[RECEIVER_T]] 

167 ) = weakref.ref # type: ignore[assignment] 

168 receiver_object: RECEIVER_T | object = receiver 

169 # Check for bound methods 

170 if hasattr(receiver, "__self__") and hasattr(receiver, "__func__"): 

171 ref = weakref.WeakMethod 

172 receiver_object = receiver.__self__ 

173 receiver_to_store = ref(receiver) 

174 weakref.finalize(receiver_object, self._flag_dead_receivers) 

175 else: 

176 receiver_to_store = receiver 

177 

178 # Keep a weakref to sender if possible to ensure associated receivers 

179 # are cleared if it gets garbage collected. This ensures there is no 

180 # id(sender) collisions for distinct senders with non-overlapping 

181 # lifetimes. 

182 sender_ref: object | None = None 

183 if sender is not None: 

184 try: 

185 sender_ref = weakref.ref(sender, self._flag_dead_receivers) 

186 except TypeError: 

187 pass 

188 

189 with self.lock: 

190 self._clear_dead_receivers() 

191 if not any(r_key == lookup_key for r_key, _, _, _ in self.receivers): 

192 self.receivers.append( 

193 (lookup_key, receiver_to_store, sender_ref, is_async) 

194 ) 

195 self.sender_receivers_cache.clear() 

196 

197 def disconnect( 

198 self, 

199 receiver: RECEIVER_T | None = None, 

200 sender: Any | None = None, 

201 dispatch_uid: Hashable | None = None, 

202 ) -> bool: 

203 """ 

204 Disconnect receiver from sender for signal. 

205 

206 If weak references are used, disconnect need not be called. The receiver 

207 will be removed from dispatch automatically. 

208 

209 Arguments: 

210 

211 receiver 

212 The registered receiver to disconnect. May be none if 

213 dispatch_uid is specified. 

214 

215 sender 

216 The registered sender to disconnect 

217 

218 dispatch_uid 

219 the unique identifier of the receiver to disconnect 

220 """ 

221 lookup_key: LOOKUP_KEY_T 

222 if dispatch_uid: 

223 lookup_key = ( 

224 dispatch_uid, 

225 _make_id(sender), 

226 ) 

227 else: 

228 lookup_key = (_make_id(receiver), _make_id(sender)) 

229 

230 disconnected: bool = False 

231 with self.lock: 

232 self._clear_dead_receivers() 

233 for index in range(len(self.receivers)): 

234 r_key, *_ = self.receivers[index] 

235 if r_key == lookup_key: 

236 disconnected = True 

237 del self.receivers[index] 

238 break 

239 self.sender_receivers_cache.clear() 

240 return disconnected 

241 

242 def has_listeners(self, sender: object | None = None) -> bool: 

243 sync_receivers, async_receivers = self._live_receivers(sender) 

244 return bool(sync_receivers) or bool(async_receivers) 

245 

246 def send( 

247 self, sender: object, **named: object 

248 ) -> list[tuple[RECEIVER_T, object | None]]: 

249 """ 

250 Send signal from sender to all connected receivers. 

251 

252 If any receiver raises an error, the error propagates back through send, 

253 terminating the dispatch loop. So it's possible that all receivers 

254 won't be called if an error is raised. 

255 

256 If any receivers are asynchronous, they are called after all the 

257 synchronous receivers via a single call to async_to_sync(). They are 

258 also executed concurrently with asyncio.gather(). 

259 

260 Arguments: 

261 

262 sender 

263 The sender of the signal. Either a specific object or None. 

264 

265 named 

266 Named arguments which will be passed to receivers. 

267 

268 Return a list of tuple pairs [(receiver, response), ... ]. 

269 """ 

270 if ( 

271 not self.receivers 

272 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS 

273 ): 

274 return [] 

275 responses: list[tuple[RECEIVER_T, Any | None]] = [] 

276 sync_receivers, async_receivers = self._live_receivers(sender) 

277 for receiver in sync_receivers: 

278 response = receiver(signal=self, sender=sender, **named) 

279 responses.append((receiver, response)) 

280 if async_receivers: 

281 

282 async def asend() -> Iterator[tuple[RECEIVER_T, object | None]]: 

283 async_responses = await asyncio.gather( 

284 *( 

285 receiver(signal=self, sender=sender, **named) 

286 for receiver in async_receivers 

287 ) 

288 ) 

289 return zip(async_receivers, async_responses) 

290 

291 responses.extend(async_to_sync(asend)()) 

292 return responses 

293 

294 async def asend( 

295 self, sender: object, **named: object 

296 ) -> list[tuple[RECEIVER_T, Any | None]]: 

297 """ 

298 Send signal from sender to all connected receivers in async mode. 

299 

300 All sync receivers will be wrapped by sync_to_async() 

301 If any receiver raises an error, the error propagates back through 

302 send, terminating the dispatch loop. So it's possible that all 

303 receivers won't be called if an error is raised. 

304 

305 If any receivers are synchronous, they are grouped and called behind a 

306 sync_to_async() adaption before executing any asynchronous receivers. 

307 

308 If any receivers are asynchronous, they are grouped and executed 

309 concurrently with asyncio.gather(). 

310 

311 Arguments: 

312 

313 sender 

314 The sender of the signal. Either a specific object or None. 

315 

316 named 

317 Named arguments which will be passed to receivers. 

318 

319 Return a list of tuple pairs [(receiver, response), ...]. 

320 """ 

321 if ( 

322 not self.receivers 

323 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS 

324 ): 

325 return [] 

326 sync_receivers, async_receivers = self._live_receivers(sender) 

327 if sync_receivers: 

328 

329 @sync_to_async 

330 def sync_send() -> list[tuple[RECEIVER_T, object | None]]: 

331 responses = [] 

332 for receiver in sync_receivers: 

333 response = receiver(signal=self, sender=sender, **named) 

334 responses.append((receiver, response)) 

335 return responses 

336 

337 else: 

338 

339 async def sync_send(): 

340 return [] 

341 

342 responses, async_responses = await asyncio.gather( 

343 sync_send(), 

344 asyncio.gather( 

345 *( 

346 receiver(signal=self, sender=sender, **named) 

347 for receiver in async_receivers 

348 ) 

349 ), 

350 ) 

351 responses.extend(zip(async_receivers, async_responses)) 

352 return responses 

353 

354 def _log_robust_failure(self, receiver: RECEIVER_T, err: Exception) -> None: 

355 logger.error( 

356 "Error calling %s in Signal.send_robust() (%s)", 

357 receiver.__qualname__, 

358 err, 

359 exc_info=err, 

360 ) 

361 

362 def send_robust( 

363 self, sender: object, **named: object 

364 ) -> list[tuple[RECEIVER_T, Exception | object | None]]: 

365 """ 

366 Send signal from sender to all connected receivers catching errors. 

367 

368 If any receivers are asynchronous, they are called after all the 

369 synchronous receivers via a single call to async_to_sync(). They are 

370 also executed concurrently with asyncio.gather(). 

371 

372 Arguments: 

373 

374 sender 

375 The sender of the signal. Can be any Python object (normally one 

376 registered with a connect if you actually want something to 

377 occur). 

378 

379 named 

380 Named arguments which will be passed to receivers. 

381 

382 Return a list of tuple pairs [(receiver, response), ... ]. 

383 

384 If any receiver raises an error (specifically any subclass of 

385 Exception), return the error instance as the result for that receiver. 

386 """ 

387 if ( 

388 not self.receivers 

389 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS 

390 ): 

391 return [] 

392 

393 # Call each receiver with whatever arguments it can accept. 

394 # Return a list of tuple pairs [(receiver, response), ... ]. 

395 responses: list[tuple[RECEIVER_T, Exception | Any | None]] = [] 

396 sync_receivers, async_receivers = self._live_receivers(sender) 

397 for receiver in sync_receivers: 

398 try: 

399 response: Any = receiver(signal=self, sender=sender, **named) 

400 except Exception as err: 

401 self._log_robust_failure(receiver, err) 

402 responses.append((receiver, err)) 

403 else: 

404 responses.append((receiver, response)) 

405 if async_receivers: 

406 

407 async def asend_and_wrap_exception(receiver: RECEIVER_T) -> object: 

408 try: 

409 response = await receiver(signal=self, sender=sender, **named) 

410 except Exception as err: 

411 self._log_robust_failure(receiver, err) 

412 return err 

413 return response 

414 

415 async def asend() -> Iterator[tuple[RECEIVER_T, object | None]]: 

416 async_responses = await asyncio.gather( 

417 *( 

418 asend_and_wrap_exception(receiver) 

419 for receiver in async_receivers 

420 ) 

421 ) 

422 return zip(async_receivers, async_responses) 

423 

424 responses.extend(async_to_sync(asend)()) 

425 return responses 

426 

427 async def asend_robust( 

428 self, sender: object, **named: object 

429 ) -> list[tuple[RECEIVER_T, Exception | object | None]]: 

430 """ 

431 Send signal from sender to all connected receivers catching errors. 

432 

433 If any receivers are synchronous, they are grouped and called behind a 

434 sync_to_async() adaption before executing any asynchronous receivers. 

435 

436 If any receivers are asynchronous, they are grouped and executed 

437 concurrently with asyncio.gather. 

438 

439 Arguments: 

440 

441 sender 

442 The sender of the signal. Can be any Python object (normally one 

443 registered with a connect if you actually want something to 

444 occur). 

445 

446 named 

447 Named arguments which will be passed to receivers. 

448 

449 Return a list of tuple pairs [(receiver, response), ... ]. 

450 

451 If any receiver raises an error (specifically any subclass of 

452 Exception), return the error instance as the result for that receiver. 

453 """ 

454 if ( 

455 not self.receivers 

456 or self.sender_receivers_cache.get(sender) is NO_RECEIVERS 

457 ): 

458 return [] 

459 

460 # Call each receiver with whatever arguments it can accept. 

461 # Return a list of tuple pairs [(receiver, response), ... ]. 

462 sync_receivers: list[RECEIVER_T] 

463 async_receivers: list[RECEIVER_T] 

464 sync_receivers, async_receivers = self._live_receivers(sender) 

465 

466 if sync_receivers: 

467 

468 @sync_to_async 

469 def sync_send() -> list[tuple[RECEIVER_T, object | Exception | None]]: 

470 responses: list[tuple[RECEIVER_T, object | Exception]] = [] 

471 for receiver in sync_receivers: 

472 try: 

473 response = receiver(signal=self, sender=sender, **named) 

474 except Exception as err: 

475 self._log_robust_failure(receiver, err) 

476 responses.append((receiver, err)) 

477 else: 

478 responses.append((receiver, response)) 

479 return responses 

480 

481 else: 

482 

483 async def sync_send(): 

484 return [] 

485 

486 async def asend_and_wrap_exception(receiver: RECEIVER_T) -> object | None: 

487 try: 

488 response = await receiver(signal=self, sender=sender, **named) 

489 except Exception as err: 

490 self._log_robust_failure(receiver, err) 

491 return err 

492 return response 

493 

494 responses, async_responses = await asyncio.gather( 

495 sync_send(), 

496 asyncio.gather( 

497 *(asend_and_wrap_exception(receiver) for receiver in async_receivers), 

498 ), 

499 ) 

500 responses.extend(zip(async_receivers, async_responses)) 

501 return responses 

502 

503 def _clear_dead_receivers(self) -> None: 

504 # Note: caller is assumed to hold self.lock. 

505 if self._dead_receivers: 

506 self._dead_receivers = False 

507 self.receivers = [ 

508 r 

509 for r in self.receivers 

510 if ( 

511 not (isinstance(r[1], weakref.ReferenceType) and r[1]() is None) 

512 and not (r[2] is not None and r[2]() is None) 

513 ) 

514 ] 

515 

516 def _live_receivers( 

517 self, sender: object 

518 ) -> tuple[list[RECEIVER_T], list[RECEIVER_T]]: 

519 """ 

520 Filter sequence of receivers to get resolved, live receivers. 

521 

522 This checks for weak references and resolves them, then returning only 

523 live receivers. 

524 """ 

525 type RECEIVER_LIST_T = list[tuple[RECEIVER_T | RECEIVER_REF_T, tuple[int, int] | int, bool]] 

526 receivers: ( 

527 RECEIVER_LIST_T 

528 | int 

529 | None 

530 ) = [] 

531 if self.use_caching and not self._dead_receivers: 

532 receivers = self.sender_receivers_cache.get(sender) 

533 # We could end up here with NO_RECEIVERS even if we do check this case in 

534 # .send() prior to calling _live_receivers() due to concurrent .send() call. 

535 if isinstance(receivers, int): 

536 return [], [] 

537 

538 if not receivers: 

539 with self.lock: 

540 self._clear_dead_receivers() 

541 senderkey = _make_id(sender) 

542 receivers = [] 

543 for ( 

544 (_receiverkey, r_senderkey), 

545 receiver, 

546 sender_ref, 

547 is_async, 

548 ) in self.receivers: 

549 if r_senderkey == NONE_ID or r_senderkey == senderkey: 

550 receivers.append((receiver, sender_ref, is_async)) 

551 if self.use_caching: 

552 if not receivers: 

553 self.sender_receivers_cache[sender] = NO_RECEIVERS 

554 else: 

555 # Note, we must cache the weakref versions. 

556 self.sender_receivers_cache[sender] = receivers 

557 

558 non_weak_sync_receivers = [] 

559 non_weak_async_receivers = [] 

560 for rec, sender_ref, is_async in cast(RECEIVER_LIST_T, receivers): 

561 # Skip if the receiver/sender is a dead weakref 

562 if isinstance(rec, weakref.ReferenceType): 

563 _receiver: RECEIVER_T | None = rec() 

564 if _receiver is None: 

565 continue 

566 else: 

567 _receiver = rec 

568 if sender_ref is not None and sender_ref() is None: 

569 continue 

570 if is_async: 

571 non_weak_async_receivers.append(_receiver) 

572 else: 

573 non_weak_sync_receivers.append(_receiver) 

574 return non_weak_sync_receivers, non_weak_async_receivers 

575 

576 def _flag_dead_receivers(self, _: weakref.ReferenceType[Any] | None=None) -> None: 

577 # Mark that the self.receivers list has dead weakrefs. If so, we will 

578 # clean those up in connect, disconnect and _live_receivers while 

579 # holding self.lock. Note that doing the cleanup here isn't a good 

580 # idea, _flag_dead_receivers() will be called as side effect of garbage 

581 # collection, and so the call can happen while we are already holding 

582 # self.lock. 

583 self._dead_receivers = True 

584 

585 

586def receiver(signal: Signal, **kwargs: Any) -> Callable[[Callable], Callable]: 

587 """ 

588 A decorator for connecting receivers to signals. Used by passing in the 

589 signal (or list of signals) and keyword arguments to connect:: 

590 

591 @receiver(post_save, sender=MyModel) 

592 def signal_receiver(sender, **kwargs): 

593 ... 

594 

595 @receiver([post_save, post_delete], sender=MyModel) 

596 def signals_receiver(sender, **kwargs): 

597 ... 

598 """ 

599 

600 def _decorator(func): 

601 if isinstance(signal, (list, tuple)): 

602 for s in signal: 

603 s.connect(func, **kwargs) 

604 else: 

605 signal.connect(func, **kwargs) 

606 return func 

607 

608 return _decorator