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

1 import logging

2 import threading

3 import weakref

4

5 from plain.utils.inspect import func_accepts_kwargs

6

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

8

9

10 def _make_id(target):

11 if hasattr(target, "__func__"):

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

13 return id(target)

14

15

16 NONE_ID = _make_id(None)

17

18# A marker for caching

19 NO_RECEIVERS = object()

20

21

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

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