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

1 import logging

2 import re

3 import weakref

4 from types import TracebackType

5

6 import pytest

7

8 from signals.dispatch import Signal, receiver

9 from signals.dispatch.dispatcher import _make_id

10 from signals.test.utils import garbage_collect

11

12 from lazy_settings.test.utils import override_settings

13

14

15 def receiver_1_arg(val, **kwargs):

16 return val

17

18

19 class Callable:

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

21 return val

22

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

24 return val

25

26

27 a_signal = Signal()

28 b_signal = Signal()

29 c_signal = Signal()

30 d_signal = Signal(use_caching=True)

31

32

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

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