Coverage for tests/test_dispatch/test_dispatch.py: 98%

218 statements  

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

1import logging 

2import re 

3import weakref 

4from types import TracebackType 

5 

6import pytest 

7 

8from signals.dispatch import Signal, receiver 

9from signals.dispatch.dispatcher import _make_id 

10from signals.test.utils import garbage_collect 

11 

12from lazy_settings.test.utils import override_settings 

13 

14 

15def receiver_1_arg(val, **kwargs): 

16 return val 

17 

18 

19class Callable: 

20 def __call__(self, val, **kwargs): 

21 return val 

22 

23 def a(self, val, **kwargs): 

24 return val 

25 

26 

27a_signal = Signal() 

28b_signal = Signal() 

29c_signal = Signal() 

30d_signal = Signal(use_caching=True) 

31 

32 

33class TestDispatcher: 

34 def assert_test_is_clean(self, signal:Signal): 

35 assert not signal.has_listeners() 

36 assert signal.receivers == [] 

37 

38 @override_settings(DEBUG=True) 

39 def test_cannot_connect_no_kwargs(self): 

40 def receiver_no_kwargs(sender): 

41 pass 

42 

43 msg = re.escape("Signal receivers must accept keyword arguments (**kwargs).") 

44 with pytest.raises(ValueError, match=msg): 

45 a_signal.connect(receiver_no_kwargs) 

46 self.assert_test_is_clean(a_signal) 

47 

48 @override_settings(DEBUG=True) 

49 def test_cannot_connect_non_callable(self): 

50 msg = "Signal receivers must be callable." 

51 with pytest.raises(TypeError, match=msg): 

52 a_signal.connect(object()) 

53 self.assert_test_is_clean(a_signal) 

54 

55 def test_send(self): 

56 a_signal.connect(receiver_1_arg, sender=self) 

57 result = a_signal.send(sender=self, val="test") 

58 assert result == [(receiver_1_arg, "test")] 

59 a_signal.disconnect(receiver_1_arg, sender=self) 

60 self.assert_test_is_clean(a_signal) 

61 

62 def test_send_no_receivers(self): 

63 result = a_signal.send(sender=self, val="test") 

64 assert result == [] 

65 

66 def test_send_connected_no_sender(self): 

67 a_signal.connect(receiver_1_arg) 

68 result = a_signal.send(sender=self, val="test") 

69 assert result == [(receiver_1_arg, "test")] 

70 a_signal.disconnect(receiver_1_arg) 

71 self.assert_test_is_clean(a_signal) 

72 

73 def test_send_different_no_sender(self): 

74 a_signal.connect(receiver_1_arg, sender=object) 

75 result = a_signal.send(sender=self, val="test") 

76 assert result == [] 

77 a_signal.disconnect(receiver_1_arg, sender=object) 

78 self.assert_test_is_clean(a_signal) 

79 

80 def test_unweakrefable_sender(self): 

81 sender = object() 

82 a_signal.connect(receiver_1_arg, sender=sender) 

83 result = a_signal.send(sender=sender, val="test") 

84 assert result == [(receiver_1_arg, "test")] 

85 a_signal.disconnect(receiver_1_arg, sender=sender) 

86 self.assert_test_is_clean(a_signal) 

87 

88 def test_garbage_collected_receiver(self): 

89 a = Callable() 

90 a_signal.connect(a.a, sender=self) 

91 del a 

92 garbage_collect() 

93 result = a_signal.send(sender=self, val="test") 

94 assert result == [] 

95 self.assert_test_is_clean(a_signal) 

96 

97 def test_garbage_collected_sender(self, mocker): 

98 signal = Signal() 

99 

100 class Sender: 

101 pass 

102 

103 def make_id(target): 

104 """ 

105 Simulate id() reuse for distinct senders with non-overlapping 

106 lifetimes that would require memory contention to reproduce. 

107 """ 

108 if isinstance(target, Sender): 

109 return 0 

110 return _make_id(target) 

111 

112 def first_receiver(attempt, **kwargs): 

113 return attempt 

114 

115 def second_receiver(attempt, **kwargs): 

116 return attempt 

117 

118 mocker.patch("signals.dispatch.dispatcher._make_id", make_id) 

119 sender = Sender() 

120 signal.connect(first_receiver, sender) 

121 result = signal.send(sender, attempt="first") 

122 assert result == [(first_receiver, "first")] 

123 

124 del sender 

125 garbage_collect() 

126 

127 sender = Sender() 

128 signal.connect(second_receiver, sender) 

129 result = signal.send(sender, attempt="second") 

130 assert result == [(second_receiver, "second")] 

131 

132 def test_cached_garbaged_collected(self): 

133 """ 

134 Make sure signal caching sender receivers don't prevent garbage 

135 collection of senders. 

136 """ 

137 

138 class sender: 

139 pass 

140 

141 wref = weakref.ref(sender) 

142 d_signal.connect(receiver_1_arg) 

143 d_signal.send(sender, val="garbage") 

144 del sender 

145 garbage_collect() 

146 try: 

147 assert wref() is None 

148 finally: 

149 # Disconnect after reference check since it flushes the tested cache. 

150 d_signal.disconnect(receiver_1_arg) 

151 

152 def test_multiple_registration(self): 

153 a = Callable() 

154 a_signal.connect(a) 

155 a_signal.connect(a) 

156 a_signal.connect(a) 

157 a_signal.connect(a) 

158 a_signal.connect(a) 

159 a_signal.connect(a) 

160 result = a_signal.send(sender=self, val="test") 

161 assert len(result) == 1 

162 assert len(a_signal.receivers) == 1 

163 del a 

164 del result 

165 garbage_collect() 

166 self.assert_test_is_clean(a_signal) 

167 

168 def test_uid_registration(self): 

169 def uid_based_receiver_1(**kwargs): 

170 pass 

171 

172 def uid_based_receiver_2(**kwargs): 

173 pass 

174 

175 a_signal.connect(uid_based_receiver_1, dispatch_uid="uid") 

176 a_signal.connect(uid_based_receiver_2, dispatch_uid="uid") 

177 assert len(a_signal.receivers) == 1 

178 a_signal.disconnect(dispatch_uid="uid") 

179 self.assert_test_is_clean(a_signal) 

180 

181 def test_send_robust_success(self): 

182 a_signal.connect(receiver_1_arg) 

183 result = a_signal.send_robust(sender=self, val="test") 

184 assert result == [(receiver_1_arg, "test")] 

185 a_signal.disconnect(receiver_1_arg) 

186 self.assert_test_is_clean(a_signal) 

187 

188 def test_send_robust_no_receivers(self): 

189 result = a_signal.send_robust(sender=self, val="test") 

190 assert result == [] 

191 

192 def test_send_robust_ignored_sender(self): 

193 a_signal.connect(receiver_1_arg) 

194 result = a_signal.send_robust(sender=self, val="test") 

195 assert result == [(receiver_1_arg, "test")] 

196 a_signal.disconnect(receiver_1_arg) 

197 self.assert_test_is_clean(a_signal) 

198 

199 def test_send_robust_fail(self, caplog): 

200 def fails(val, **kwargs): 

201 raise ValueError("this") 

202 

203 a_signal.connect(fails) 

204 try: 

205 with caplog.at_level(logging.ERROR, logger="signals.dispatch"): 

206 result = a_signal.send_robust(sender=self, val="test") 

207 err = result[0][1] 

208 assert isinstance(err, ValueError) 

209 assert err.args == ("this",) 

210 assert hasattr(err, "__traceback__") 

211 assert isinstance(err.__traceback__, TracebackType) 

212 

213 log_record = caplog.records[0] 

214 assert ( 

215 log_record.getMessage() == 

216 "Error calling " 

217 "TestDispatcher.test_send_robust_fail.<locals>.fails in " 

218 "Signal.send_robust() (this)" 

219 ) 

220 assert log_record.exc_info 

221 _, exc_value, _ = log_record.exc_info 

222 assert isinstance(exc_value, ValueError) 

223 assert str(exc_value) == "this" 

224 finally: 

225 a_signal.disconnect(fails) 

226 

227 self.assert_test_is_clean(a_signal) 

228 

229 def test_disconnection(self): 

230 receiver_1 = Callable() 

231 receiver_2 = Callable() 

232 receiver_3 = Callable() 

233 a_signal.connect(receiver_1) 

234 a_signal.connect(receiver_2) 

235 a_signal.connect(receiver_3) 

236 a_signal.disconnect(receiver_1) 

237 del receiver_2 

238 garbage_collect() 

239 a_signal.disconnect(receiver_3) 

240 self.assert_test_is_clean(a_signal) 

241 

242 def test_values_returned_by_disconnection(self): 

243 receiver_1 = Callable() 

244 receiver_2 = Callable() 

245 a_signal.connect(receiver_1) 

246 receiver_1_disconnected = a_signal.disconnect(receiver_1) 

247 receiver_2_disconnected = a_signal.disconnect(receiver_2) 

248 assert receiver_1_disconnected 

249 assert receiver_2_disconnected is False 

250 self.assert_test_is_clean(a_signal) 

251 

252 def test_has_listeners(self): 

253 assert a_signal.has_listeners() is False 

254 assert a_signal.has_listeners() is False 

255 receiver_1 = Callable() 

256 a_signal.connect(receiver_1) 

257 assert a_signal.has_listeners() 

258 assert a_signal.has_listeners(sender=object()) 

259 a_signal.disconnect(receiver_1) 

260 assert a_signal.has_listeners() is False 

261 assert a_signal.has_listeners(sender=object()) is False 

262 

263class TestReceiver: 

264 def test_receiver_single_signal(self): 

265 @receiver(a_signal) 

266 def f(val, **kwargs): 

267 self.state = val 

268 

269 self.state = False 

270 a_signal.send(sender=self, val=True) 

271 assert self.state is True 

272 

273 def test_receiver_signal_list(self): 

274 @receiver([a_signal, b_signal, c_signal]) 

275 def f(val, **kwargs): 

276 self.state.append(val) 

277 

278 self.state = [] 

279 a_signal.send(sender=self, val="a") 

280 c_signal.send(sender=self, val="c") 

281 b_signal.send(sender=self, val="b") 

282 assert "a" in self.state 

283 assert "b" in self.state 

284 assert "c" in self.state