Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/blinker/base.py: 35%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

204 statements  

1from __future__ import annotations 

2 

3import collections.abc as c 

4import typing as t 

5import warnings 

6import weakref 

7from collections import defaultdict 

8from contextlib import AbstractContextManager 

9from contextlib import contextmanager 

10from functools import cached_property 

11from inspect import iscoroutinefunction 

12from weakref import WeakValueDictionary 

13 

14from ._utilities import make_id 

15from ._utilities import make_ref 

16from ._utilities import Symbol 

17 

18if t.TYPE_CHECKING: 

19 F = t.TypeVar("F", bound=c.Callable[..., t.Any]) 

20 

21ANY = Symbol("ANY") 

22"""Symbol for "any sender".""" 

23 

24ANY_ID = 0 

25 

26 

27class Signal: 

28 """A notification emitter. 

29 

30 :param doc: The docstring for the signal. 

31 """ 

32 

33 ANY = ANY 

34 """An alias for the :data:`~blinker.ANY` sender symbol.""" 

35 

36 set_class: type[set[t.Any]] = set 

37 """The set class to use for tracking connected receivers and senders. 

38 Python's ``set`` is unordered. If receivers must be dispatched in the order 

39 they were connected, an ordered set implementation can be used. 

40 

41 .. versionadded:: 1.7 

42 """ 

43 

44 @cached_property 

45 def receiver_connected(self) -> Signal: 

46 """Emitted at the end of each :meth:`connect` call. 

47 

48 The signal sender is the signal instance, and the :meth:`connect` 

49 arguments are passed through: ``receiver``, ``sender``, and ``weak``. 

50 

51 .. versionadded:: 1.2 

52 """ 

53 return Signal(doc="Emitted after a receiver connects.") 

54 

55 @cached_property 

56 def receiver_disconnected(self) -> Signal: 

57 """Emitted at the end of each :meth:`disconnect` call. 

58 

59 The sender is the signal instance, and the :meth:`disconnect` arguments 

60 are passed through: ``receiver`` and ``sender``. 

61 

62 This signal is emitted **only** when :meth:`disconnect` is called 

63 explicitly. This signal cannot be emitted by an automatic disconnect 

64 when a weakly referenced receiver or sender goes out of scope, as the 

65 instance is no longer be available to be used as the sender for this 

66 signal. 

67 

68 An alternative approach is available by subscribing to 

69 :attr:`receiver_connected` and setting up a custom weakref cleanup 

70 callback on weak receivers and senders. 

71 

72 .. versionadded:: 1.2 

73 """ 

74 return Signal(doc="Emitted after a receiver disconnects.") 

75 

76 def __init__(self, doc: str | None = None) -> None: 

77 if doc: 

78 self.__doc__ = doc 

79 

80 self.receivers: dict[ 

81 t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any] 

82 ] = {} 

83 """The map of connected receivers. Useful to quickly check if any 

84 receivers are connected to the signal: ``if s.receivers:``. The 

85 structure and data is not part of the public API, but checking its 

86 boolean value is. 

87 """ 

88 

89 self.is_muted: bool = False 

90 self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) 

91 self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) 

92 self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {} 

93 

94 def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F: 

95 """Connect ``receiver`` to be called when the signal is sent by 

96 ``sender``. 

97 

98 :param receiver: The callable to call when :meth:`send` is called with 

99 the given ``sender``, passing ``sender`` as a positional argument 

100 along with any extra keyword arguments. 

101 :param sender: Any object or :data:`ANY`. ``receiver`` will only be 

102 called when :meth:`send` is called with this sender. If ``ANY``, the 

103 receiver will be called for any sender. A receiver may be connected 

104 to multiple senders by calling :meth:`connect` multiple times. 

105 :param weak: Track the receiver with a :mod:`weakref`. The receiver will 

106 be automatically disconnected when it is garbage collected. When 

107 connecting a receiver defined within a function, set to ``False``, 

108 otherwise it will be disconnected when the function scope ends. 

109 """ 

110 receiver_id = make_id(receiver) 

111 sender_id = ANY_ID if sender is ANY else make_id(sender) 

112 

113 if weak: 

114 self.receivers[receiver_id] = make_ref( 

115 receiver, self._make_cleanup_receiver(receiver_id) 

116 ) 

117 else: 

118 self.receivers[receiver_id] = receiver 

119 

120 self._by_sender[sender_id].add(receiver_id) 

121 self._by_receiver[receiver_id].add(sender_id) 

122 

123 if sender is not ANY and sender_id not in self._weak_senders: 

124 # store a cleanup for weakref-able senders 

125 try: 

126 self._weak_senders[sender_id] = make_ref( 

127 sender, self._make_cleanup_sender(sender_id) 

128 ) 

129 except TypeError: 

130 pass 

131 

132 if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers: 

133 try: 

134 self.receiver_connected.send( 

135 self, receiver=receiver, sender=sender, weak=weak 

136 ) 

137 except TypeError: 

138 # TODO no explanation or test for this 

139 self.disconnect(receiver, sender) 

140 raise 

141 

142 if _receiver_connected.receivers and self is not _receiver_connected: 

143 try: 

144 _receiver_connected.send( 

145 self, receiver_arg=receiver, sender_arg=sender, weak_arg=weak 

146 ) 

147 except TypeError: 

148 self.disconnect(receiver, sender) 

149 raise 

150 

151 return receiver 

152 

153 def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]: 

154 """Connect the decorated function to be called when the signal is sent 

155 by ``sender``. 

156 

157 The decorated function will be called when :meth:`send` is called with 

158 the given ``sender``, passing ``sender`` as a positional argument along 

159 with any extra keyword arguments. 

160 

161 :param sender: Any object or :data:`ANY`. ``receiver`` will only be 

162 called when :meth:`send` is called with this sender. If ``ANY``, the 

163 receiver will be called for any sender. A receiver may be connected 

164 to multiple senders by calling :meth:`connect` multiple times. 

165 :param weak: Track the receiver with a :mod:`weakref`. The receiver will 

166 be automatically disconnected when it is garbage collected. When 

167 connecting a receiver defined within a function, set to ``False``, 

168 otherwise it will be disconnected when the function scope ends.= 

169 

170 .. versionadded:: 1.1 

171 """ 

172 

173 def decorator(fn: F) -> F: 

174 self.connect(fn, sender, weak) 

175 return fn 

176 

177 return decorator 

178 

179 @contextmanager 

180 def connected_to( 

181 self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY 

182 ) -> c.Generator[None, None, None]: 

183 """A context manager that temporarily connects ``receiver`` to the 

184 signal while a ``with`` block executes. When the block exits, the 

185 receiver is disconnected. Useful for tests. 

186 

187 :param receiver: The callable to call when :meth:`send` is called with 

188 the given ``sender``, passing ``sender`` as a positional argument 

189 along with any extra keyword arguments. 

190 :param sender: Any object or :data:`ANY`. ``receiver`` will only be 

191 called when :meth:`send` is called with this sender. If ``ANY``, the 

192 receiver will be called for any sender. 

193 

194 .. versionadded:: 1.1 

195 """ 

196 self.connect(receiver, sender=sender, weak=False) 

197 

198 try: 

199 yield None 

200 finally: 

201 self.disconnect(receiver) 

202 

203 @contextmanager 

204 def muted(self) -> c.Generator[None, None, None]: 

205 """A context manager that temporarily disables the signal. No receivers 

206 will be called if the signal is sent, until the ``with`` block exits. 

207 Useful for tests. 

208 """ 

209 self.is_muted = True 

210 

211 try: 

212 yield None 

213 finally: 

214 self.is_muted = False 

215 

216 def temporarily_connected_to( 

217 self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY 

218 ) -> AbstractContextManager[None]: 

219 """Deprecated alias for :meth:`connected_to`. 

220 

221 .. deprecated:: 1.1 

222 Renamed to ``connected_to``. Will be removed in Blinker 1.9. 

223 

224 .. versionadded:: 0.9 

225 """ 

226 warnings.warn( 

227 "'temporarily_connected_to' is renamed to 'connected_to'. The old name is" 

228 " deprecated and will be removed in Blinker 1.9.", 

229 DeprecationWarning, 

230 stacklevel=2, 

231 ) 

232 return self.connected_to(receiver, sender) 

233 

234 def send( 

235 self, 

236 sender: t.Any | None = None, 

237 /, 

238 *, 

239 _async_wrapper: c.Callable[ 

240 [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any] 

241 ] 

242 | None = None, 

243 **kwargs: t.Any, 

244 ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: 

245 """Call all receivers that are connected to the given ``sender`` 

246 or :data:`ANY`. Each receiver is called with ``sender`` as a positional 

247 argument along with any extra keyword arguments. Return a list of 

248 ``(receiver, return value)`` tuples. 

249 

250 The order receivers are called is undefined, but can be influenced by 

251 setting :attr:`set_class`. 

252 

253 If a receiver raises an exception, that exception will propagate up. 

254 This makes debugging straightforward, with an assumption that correctly 

255 implemented receivers will not raise. 

256 

257 :param sender: Call receivers connected to this sender, in addition to 

258 those connected to :data:`ANY`. 

259 :param _async_wrapper: Will be called on any receivers that are async 

260 coroutines to turn them into sync callables. For example, could run 

261 the receiver with an event loop. 

262 :param kwargs: Extra keyword arguments to pass to each receiver. 

263 

264 .. versionchanged:: 1.7 

265 Added the ``_async_wrapper`` argument. 

266 """ 

267 if self.is_muted: 

268 return [] 

269 

270 results = [] 

271 

272 for receiver in self.receivers_for(sender): 

273 if iscoroutinefunction(receiver): 

274 if _async_wrapper is None: 

275 raise RuntimeError("Cannot send to a coroutine function.") 

276 

277 result = _async_wrapper(receiver)(sender, **kwargs) 

278 else: 

279 result = receiver(sender, **kwargs) 

280 

281 results.append((receiver, result)) 

282 

283 return results 

284 

285 async def send_async( 

286 self, 

287 sender: t.Any | None = None, 

288 /, 

289 *, 

290 _sync_wrapper: c.Callable[ 

291 [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]] 

292 ] 

293 | None = None, 

294 **kwargs: t.Any, 

295 ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: 

296 """Await all receivers that are connected to the given ``sender`` 

297 or :data:`ANY`. Each receiver is called with ``sender`` as a positional 

298 argument along with any extra keyword arguments. Return a list of 

299 ``(receiver, return value)`` tuples. 

300 

301 The order receivers are called is undefined, but can be influenced by 

302 setting :attr:`set_class`. 

303 

304 If a receiver raises an exception, that exception will propagate up. 

305 This makes debugging straightforward, with an assumption that correctly 

306 implemented receivers will not raise. 

307 

308 :param sender: Call receivers connected to this sender, in addition to 

309 those connected to :data:`ANY`. 

310 :param _sync_wrapper: Will be called on any receivers that are sync 

311 callables to turn them into async coroutines. For example, 

312 could call the receiver in a thread. 

313 :param kwargs: Extra keyword arguments to pass to each receiver. 

314 

315 .. versionadded:: 1.7 

316 """ 

317 if self.is_muted: 

318 return [] 

319 

320 results = [] 

321 

322 for receiver in self.receivers_for(sender): 

323 if not iscoroutinefunction(receiver): 

324 if _sync_wrapper is None: 

325 raise RuntimeError("Cannot send to a non-coroutine function.") 

326 

327 result = await _sync_wrapper(receiver)(sender, **kwargs) 

328 else: 

329 result = await receiver(sender, **kwargs) 

330 

331 results.append((receiver, result)) 

332 

333 return results 

334 

335 def has_receivers_for(self, sender: t.Any) -> bool: 

336 """Check if there is at least one receiver that will be called with the 

337 given ``sender``. A receiver connected to :data:`ANY` will always be 

338 called, regardless of sender. Does not check if weakly referenced 

339 receivers are still live. See :meth:`receivers_for` for a stronger 

340 search. 

341 

342 :param sender: Check for receivers connected to this sender, in addition 

343 to those connected to :data:`ANY`. 

344 """ 

345 if not self.receivers: 

346 return False 

347 

348 if self._by_sender[ANY_ID]: 

349 return True 

350 

351 if sender is ANY: 

352 return False 

353 

354 return make_id(sender) in self._by_sender 

355 

356 def receivers_for( 

357 self, sender: t.Any 

358 ) -> c.Generator[c.Callable[..., t.Any], None, None]: 

359 """Yield each receiver to be called for ``sender``, in addition to those 

360 to be called for :data:`ANY`. Weakly referenced receivers that are not 

361 live will be disconnected and skipped. 

362 

363 :param sender: Yield receivers connected to this sender, in addition 

364 to those connected to :data:`ANY`. 

365 """ 

366 # TODO: test receivers_for(ANY) 

367 if not self.receivers: 

368 return 

369 

370 sender_id = make_id(sender) 

371 

372 if sender_id in self._by_sender: 

373 ids = self._by_sender[ANY_ID] | self._by_sender[sender_id] 

374 else: 

375 ids = self._by_sender[ANY_ID].copy() 

376 

377 for receiver_id in ids: 

378 receiver = self.receivers.get(receiver_id) 

379 

380 if receiver is None: 

381 continue 

382 

383 if isinstance(receiver, weakref.ref): 

384 strong = receiver() 

385 

386 if strong is None: 

387 self._disconnect(receiver_id, ANY_ID) 

388 continue 

389 

390 yield strong 

391 else: 

392 yield receiver 

393 

394 def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None: 

395 """Disconnect ``receiver`` from being called when the signal is sent by 

396 ``sender``. 

397 

398 :param receiver: A connected receiver callable. 

399 :param sender: Disconnect from only this sender. By default, disconnect 

400 from all senders. 

401 """ 

402 sender_id: c.Hashable 

403 

404 if sender is ANY: 

405 sender_id = ANY_ID 

406 else: 

407 sender_id = make_id(sender) 

408 

409 receiver_id = make_id(receiver) 

410 self._disconnect(receiver_id, sender_id) 

411 

412 if ( 

413 "receiver_disconnected" in self.__dict__ 

414 and self.receiver_disconnected.receivers 

415 ): 

416 self.receiver_disconnected.send(self, receiver=receiver, sender=sender) 

417 

418 def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None: 

419 if sender_id == ANY_ID: 

420 if self._by_receiver.pop(receiver_id, None) is not None: 

421 for bucket in self._by_sender.values(): 

422 bucket.discard(receiver_id) 

423 

424 self.receivers.pop(receiver_id, None) 

425 else: 

426 self._by_sender[sender_id].discard(receiver_id) 

427 self._by_receiver[receiver_id].discard(sender_id) 

428 

429 def _make_cleanup_receiver( 

430 self, receiver_id: c.Hashable 

431 ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]: 

432 """Create a callback function to disconnect a weakly referenced 

433 receiver when it is garbage collected. 

434 """ 

435 

436 def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None: 

437 self._disconnect(receiver_id, ANY_ID) 

438 

439 return cleanup 

440 

441 def _make_cleanup_sender( 

442 self, sender_id: c.Hashable 

443 ) -> c.Callable[[weakref.ref[t.Any]], None]: 

444 """Create a callback function to disconnect all receivers for a weakly 

445 referenced sender when it is garbage collected. 

446 """ 

447 assert sender_id != ANY_ID 

448 

449 def cleanup(ref: weakref.ref[t.Any]) -> None: 

450 self._weak_senders.pop(sender_id, None) 

451 

452 for receiver_id in self._by_sender.pop(sender_id, ()): 

453 self._by_receiver[receiver_id].discard(sender_id) 

454 

455 return cleanup 

456 

457 def _cleanup_bookkeeping(self) -> None: 

458 """Prune unused sender/receiver bookkeeping. Not threadsafe. 

459 

460 Connecting & disconnecting leaves behind a small amount of bookkeeping 

461 data. Typical workloads using Blinker, for example in most web apps, 

462 Flask, CLI scripts, etc., are not adversely affected by this 

463 bookkeeping. 

464 

465 With a long-running process performing dynamic signal routing with high 

466 volume, e.g. connecting to function closures, senders are all unique 

467 object instances. Doing all of this over and over may cause memory usage 

468 to grow due to extraneous bookkeeping. (An empty ``set`` for each stale 

469 sender/receiver pair.) 

470 

471 This method will prune that bookkeeping away, with the caveat that such 

472 pruning is not threadsafe. The risk is that cleanup of a fully 

473 disconnected receiver/sender pair occurs while another thread is 

474 connecting that same pair. If you are in the highly dynamic, unique 

475 receiver/sender situation that has lead you to this method, that failure 

476 mode is perhaps not a big deal for you. 

477 """ 

478 for mapping in (self._by_sender, self._by_receiver): 

479 for ident, bucket in list(mapping.items()): 

480 if not bucket: 

481 mapping.pop(ident, None) 

482 

483 def _clear_state(self) -> None: 

484 """Disconnect all receivers and senders. Useful for tests.""" 

485 self._weak_senders.clear() 

486 self.receivers.clear() 

487 self._by_sender.clear() 

488 self._by_receiver.clear() 

489 

490 

491_receiver_connected = Signal( 

492 """\ 

493Sent by a :class:`Signal` after a receiver connects. 

494 

495:argument: the Signal that was connected to 

496:keyword receiver_arg: the connected receiver 

497:keyword sender_arg: the sender to connect to 

498:keyword weak_arg: true if the connection to receiver_arg is a weak reference 

499 

500.. deprecated:: 1.2 

501 Individual signals have their own :attr:`~Signal.receiver_connected` and 

502 :attr:`~Signal.receiver_disconnected` signals with a slightly simplified 

503 call signature. This global signal will be removed in Blinker 1.9. 

504""" 

505) 

506 

507 

508class NamedSignal(Signal): 

509 """A named generic notification emitter. The name is not used by the signal 

510 itself, but matches the key in the :class:`Namespace` that it belongs to. 

511 

512 :param name: The name of the signal within the namespace. 

513 :param doc: The docstring for the signal. 

514 """ 

515 

516 def __init__(self, name: str, doc: str | None = None) -> None: 

517 super().__init__(doc) 

518 

519 #: The name of this signal. 

520 self.name: str = name 

521 

522 def __repr__(self) -> str: 

523 base = super().__repr__() 

524 return f"{base[:-1]}; {self.name!r}>" # noqa: E702 

525 

526 

527if t.TYPE_CHECKING: 

528 

529 class PNamespaceSignal(t.Protocol): 

530 def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ... 

531 

532 # Python < 3.9 

533 _NamespaceBase = dict[str, NamedSignal] # type: ignore[misc] 

534else: 

535 _NamespaceBase = dict 

536 

537 

538class Namespace(_NamespaceBase): 

539 """A dict mapping names to signals.""" 

540 

541 def signal(self, name: str, doc: str | None = None) -> NamedSignal: 

542 """Return the :class:`NamedSignal` for the given ``name``, creating it 

543 if required. Repeated calls with the same name return the same signal. 

544 

545 :param name: The name of the signal. 

546 :param doc: The docstring of the signal. 

547 """ 

548 if name not in self: 

549 self[name] = NamedSignal(name, doc) 

550 

551 return self[name] 

552 

553 

554class _WeakNamespace(WeakValueDictionary): # type: ignore[type-arg] 

555 """A weak mapping of names to signals. 

556 

557 Automatically cleans up unused signals when the last reference goes out 

558 of scope. This namespace implementation provides similar behavior to Blinker 

559 <= 1.2. 

560 

561 .. deprecated:: 1.3 

562 Will be removed in Blinker 1.9. 

563 

564 .. versionadded:: 1.3 

565 """ 

566 

567 def __init__(self) -> None: 

568 warnings.warn( 

569 "'WeakNamespace' is deprecated and will be removed in Blinker 1.9." 

570 " Use 'Namespace' instead.", 

571 DeprecationWarning, 

572 stacklevel=2, 

573 ) 

574 super().__init__() 

575 

576 def signal(self, name: str, doc: str | None = None) -> NamedSignal: 

577 """Return the :class:`NamedSignal` for the given ``name``, creating it 

578 if required. Repeated calls with the same name return the same signal. 

579 

580 :param name: The name of the signal. 

581 :param doc: The docstring of the signal. 

582 """ 

583 if name not in self: 

584 self[name] = NamedSignal(name, doc) 

585 

586 return self[name] # type: ignore[no-any-return] 

587 

588 

589default_namespace: Namespace = Namespace() 

590"""A default :class:`Namespace` for creating named signals. :func:`signal` 

591creates a :class:`NamedSignal` in this namespace. 

592""" 

593 

594signal: PNamespaceSignal = default_namespace.signal 

595"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given 

596``name``, creating it if required. Repeated calls with the same name return the 

597same signal. 

598""" 

599 

600 

601def __getattr__(name: str) -> t.Any: 

602 if name == "receiver_connected": 

603 warnings.warn( 

604 "The global 'receiver_connected' signal is deprecated and will be" 

605 " removed in Blinker 1.9. Use 'Signal.receiver_connected' and" 

606 " 'Signal.receiver_disconnected' instead.", 

607 DeprecationWarning, 

608 stacklevel=2, 

609 ) 

610 return _receiver_connected 

611 

612 if name == "WeakNamespace": 

613 warnings.warn( 

614 "'WeakNamespace' is deprecated and will be removed in Blinker 1.9." 

615 " Use 'Namespace' instead.", 

616 DeprecationWarning, 

617 stacklevel=2, 

618 ) 

619 return _WeakNamespace 

620 

621 raise AttributeError(name)