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

1 import asyncio

2 import logging

3 import threading

4 import weakref

5 from collections.abc import Awaitable, Hashable, Iterator

6 from types import FunctionType, MethodType

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

8

9 from asgiref.sync import async_to_sync, iscoroutinefunction, sync_to_async

10

11 from lazy_settings.conf import settings

12

13 from signals import global_settings

14 from signals.utils.inspect import func_accepts_kwargs

15

16

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

18

19 settings.register(global_settings)

20

21

22@overload

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

24

25

26@overload

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

28

29

30 def _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

36 NONE_ID = _make_id(None)

37

38# A marker for caching

39 NO_RECEIVERS = 1

40

41

42 class Receiver(Protocol):

43 def __call__(

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

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

46

47 __qualname__: str

48

49

50 type RECEIVER_T = Receiver

51 type RECEIVER_REF_T = weakref.ReferenceType[RECEIVER_T]

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

53

54

55 class 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

586 def 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