Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain/plain/signals/dispatch/dispatcher.py: 78%

123 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

1import logging 

2import threading 

3import weakref 

4 

5from plain.utils.inspect import func_accepts_kwargs 

6 

7logger = logging.getLogger("plain.signals.dispatch") 

8 

9 

10def _make_id(target): 

11 if hasattr(target, "__func__"): 

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

13 return id(target) 

14 

15 

16NONE_ID = _make_id(None) 

17 

18# A marker for caching 

19NO_RECEIVERS = object() 

20 

21 

22class Signal: 

23 """ 

24 Base class for all signals 

25 

26 Internal attributes: 

27 

28 receivers 

29 { receiverkey (id) : weakref(receiver) } 

30 """ 

31 

32 def __init__(self, use_caching=False): 

33 """ 

34 Create a new signal. 

35 """ 

36 self.receivers = [] 

37 self.lock = threading.Lock() 

38 self.use_caching = use_caching 

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

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

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

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

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

44 self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {} 

45 self._dead_receivers = False 

46 

47 def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): 

48 """ 

49 Connect receiver to sender for signal. 

50 

51 Arguments: 

52 

53 receiver 

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

55 Receivers must be hashable objects. Receivers can be 

56 asynchronous. 

57 

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

59 

60 Receivers must be able to accept keyword arguments. 

61 

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

63 will not be added if another receiver was already connected 

64 with that dispatch_uid. 

65 

66 sender 

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

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

69 

70 weak 

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

72 module will attempt to use weak references to the receiver 

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

74 be used. 

75 

76 dispatch_uid 

77 An identifier used to uniquely identify a particular instance of 

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

79 anything hashable. 

80 """ 

81 from plain.runtime import settings 

82 

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

84 if settings.configured and settings.DEBUG: 

85 if not callable(receiver): 

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

87 # Check for **kwargs 

88 if not func_accepts_kwargs(receiver): 

89 raise ValueError( 

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

91 ) 

92 

93 if dispatch_uid: 

94 lookup_key = (dispatch_uid, _make_id(sender)) 

95 else: 

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

97 

98 if weak: 

99 ref = weakref.ref 

100 receiver_object = receiver 

101 # Check for bound methods 

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

103 ref = weakref.WeakMethod 

104 receiver_object = receiver.__self__ 

105 receiver = ref(receiver) 

106 weakref.finalize(receiver_object, self._remove_receiver) 

107 

108 with self.lock: 

109 self._clear_dead_receivers() 

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

111 self.receivers.append((lookup_key, receiver)) 

112 self.sender_receivers_cache.clear() 

113 

114 def disconnect(self, receiver=None, sender=None, dispatch_uid=None): 

115 """ 

116 Disconnect receiver from sender for signal. 

117 

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

119 will be removed from dispatch automatically. 

120 

121 Arguments: 

122 

123 receiver 

124 The registered receiver to disconnect. May be none if 

125 dispatch_uid is specified. 

126 

127 sender 

128 The registered sender to disconnect 

129 

130 dispatch_uid 

131 the unique identifier of the receiver to disconnect 

132 """ 

133 if dispatch_uid: 

134 lookup_key = (dispatch_uid, _make_id(sender)) 

135 else: 

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

137 

138 disconnected = False 

139 with self.lock: 

140 self._clear_dead_receivers() 

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

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

143 if r_key == lookup_key: 

144 disconnected = True 

145 del self.receivers[index] 

146 break 

147 self.sender_receivers_cache.clear() 

148 return disconnected 

149 

150 def has_listeners(self, sender=None): 

151 sync_receivers = self._live_receivers(sender) 

152 return bool(sync_receivers) 

153 

154 def send(self, sender, **named): 

155 """ 

156 Send signal from sender to all connected receivers. 

157 

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

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

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

161 

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

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

164 also executed concurrently with asyncio.gather(). 

165 

166 Arguments: 

167 

168 sender 

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

170 

171 named 

172 Named arguments which will be passed to receivers. 

173 

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

175 """ 

176 if ( 

177 not self.receivers 

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

179 ): 

180 return [] 

181 responses = [] 

182 sync_receivers = self._live_receivers(sender) 

183 for receiver in sync_receivers: 

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

185 responses.append((receiver, response)) 

186 return responses 

187 

188 def _log_robust_failure(self, receiver, err): 

189 logger.error( 

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

191 receiver.__qualname__, 

192 err, 

193 exc_info=err, 

194 ) 

195 

196 def send_robust(self, sender, **named): 

197 """ 

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

199 

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

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

202 also executed concurrently with asyncio.gather(). 

203 

204 Arguments: 

205 

206 sender 

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

208 registered with a connect if you actually want something to 

209 occur). 

210 

211 named 

212 Named arguments which will be passed to receivers. 

213 

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

215 

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

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

218 """ 

219 if ( 

220 not self.receivers 

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

222 ): 

223 return [] 

224 

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

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

227 responses = [] 

228 sync_receivers = self._live_receivers(sender) 

229 for receiver in sync_receivers: 

230 try: 

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

232 except Exception as err: 

233 self._log_robust_failure(receiver, err) 

234 responses.append((receiver, err)) 

235 else: 

236 responses.append((receiver, response)) 

237 return responses 

238 

239 def _clear_dead_receivers(self): 

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

241 if self._dead_receivers: 

242 self._dead_receivers = False 

243 self.receivers = [ 

244 r 

245 for r in self.receivers 

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

247 ] 

248 

249 def _live_receivers(self, sender): 

250 """ 

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

252 

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

254 live receivers. 

255 """ 

256 receivers = None 

257 if self.use_caching and not self._dead_receivers: 

258 receivers = self.sender_receivers_cache.get(sender) 

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

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

261 if receivers is NO_RECEIVERS: 

262 return [] 

263 if receivers is None: 

264 with self.lock: 

265 self._clear_dead_receivers() 

266 senderkey = _make_id(sender) 

267 receivers = [] 

268 for (_receiverkey, r_senderkey), receiver in self.receivers: 

269 if r_senderkey == NONE_ID or r_senderkey == senderkey: 

270 receivers.append(receiver) 

271 if self.use_caching: 

272 if not receivers: 

273 self.sender_receivers_cache[sender] = NO_RECEIVERS 

274 else: 

275 # Note, we must cache the weakref versions. 

276 self.sender_receivers_cache[sender] = receivers 

277 non_weak_sync_receivers = [] 

278 for receiver in receivers: 

279 if isinstance(receiver, weakref.ReferenceType): 

280 # Dereference the weak reference. 

281 receiver = receiver() 

282 if receiver is not None: 

283 non_weak_sync_receivers.append(receiver) 

284 else: 

285 non_weak_sync_receivers.append(receiver) 

286 return non_weak_sync_receivers 

287 

288 def _remove_receiver(self, receiver=None): 

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

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

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

292 # idea, _remove_receiver() will be called as side effect of garbage 

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

294 # self.lock. 

295 self._dead_receivers = True 

296 

297 

298def receiver(signal, **kwargs): 

299 """ 

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

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

302 

303 @receiver(post_save, sender=MyModel) 

304 def signal_receiver(sender, **kwargs): 

305 ... 

306 

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

308 def signals_receiver(sender, **kwargs): 

309 ... 

310 """ 

311 

312 def _decorator(func): 

313 if isinstance(signal, list | tuple): 

314 for s in signal: 

315 s.connect(func, **kwargs) 

316 else: 

317 signal.connect(func, **kwargs) 

318 return func 

319 

320 return _decorator