1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 """
24 Introduction
25 ============
26
27 Gramps is devided into two parts. The database code, that does not
28 require any particular GUI libraries, and the gtk-based UI code
29 that requires gtk and gnome libraries. The gtk-based code can use
30 the gobject signal support to manage callback signals but the database
31 code can not.
32
33 The module provides a subset of the signal mechanisms that are available
34 from the gobject framework. It enables the database code to use signals
35 to communicate events to any callback methods in either the database code
36 or the UI code.
37 """
38 import sys
39 import types
40 import traceback
41 import inspect
42
43 log = sys.stderr.write
44
45
46
47
48
49
50
51 -class Callback(object):
52 """
53 Callback and signal support objects.
54
55 Declaring signals
56 =================
57
58 Classes that want to emit signals need to inherit from the
59 DBCallback class and ensure that its __init__ method
60 is called. They then need to declare the signals that they
61 can emit and the types of each callbacks arguments. For
62 example::
63
64 class TestSignals(Callback):
65
66 __signals__ = {
67 'test-signal' : (int, ),
68 'test-noarg' : None
69 }
70
71 def __init__(self):
72 Callback.__init__(self)
73
74 The type signature is a tuple of types or classes. The type
75 checking code uses the isinstance method to check that the
76 argument passed to the emit call is an instance of the type
77 in the signature declaration.
78
79 If the signal does not have an argument use None as the
80 signature.
81
82 The signals will be inherited by any subclasses. Duplicate
83 signal names in subclasses are not alowed.
84
85
86 Emitting signals
87 ================
88
89 Signals are emitted using the emit method. e.g.::
90
91 def emit_signal(self):
92 self.emit('test-signal', (1, ))
93
94 The parameters are passed as a tuple so a single parameter
95 must be passed as a 1 element tuple.
96
97 Connecting callbacks to signals
98 ===============================
99
100 Attaching a callback to the signals is similar to the gtk
101 connect methods. e.g.::
102
103 # connect to a function.
104 def fn(i):
105 print 'got signal value = ', i
106
107 t = TestSignals()
108 t.connect('test-signal', fn)
109
110 # connect to a bound method
111 class C(object):
112
113 def cb_func(self, i):
114 print 'got class signal = ', 1
115
116 r = R()
117 t.connect('test-signal', r.cb_func)
118
119
120 Disconnecting callbacks
121 =======================
122
123 If you want to disconnect a callback from a signals you must remember the
124 key returned from the connect call. This key can be passed to the disconnect
125 method to remove the callback from the signals callback list.
126
127 e.g.::
128
129 t = TestSignals()
130
131 # connect to a bound method
132 class C(object):
133
134 def cb_func(self, i):
135 print 'got class signal = ', 1
136
137 r = R()
138 key = t.connect('test-signal', r.cb_func)
139
140 ...
141
142 t.disconnect(key)
143
144
145 Stopping and starting signals
146 =============================
147
148 Signals can be blocked on a per instance bassis or they can be blocked
149 for all instances of the Callback class. disable_signals() can
150 be used to block the signals for a single instance and disable_all_signals()
151 can be used to block signals for the class:
152
153 e.g.::
154
155
156 class TestSignals(Callback):
157
158 __signals__ = {
159 'test-signal' : (int, ),
160 'test-noarg' : None
161 }
162
163 def __init__(self):
164 Callback.__init__(self)
165
166 def emit_signal(self):
167 self.emit('test-signal', (1, ))
168
169 t = TestSignals()
170
171 # block signals from instance t
172 t.disable_signals()
173
174 ...
175
176 # unblock
177 t.enable_signals()
178
179 # block all signals
180 Callback.disable_all_signals()
181
182 ...
183
184 # unblock all signals
185 Callback.enable_all_signals()
186
187
188 Any signals emitted whilst signals are blocked will be lost.
189
190
191 Debugging signal callbacks
192 ==========================
193
194
195 To help with debugging the signals and callbacks you can turn on
196 lots of logging information. To switch on logging for a single
197 instance call self.enable_logging(), to switch it off again call
198 self.disable_logging(). To switch on logging for all instance
199 you can toggle Callback.__LOG_ALL to True.
200
201 """
202
203
204
205
206 __BLOCK_ALL_SIGNALS = False
207 __LOG_ALL = False
208
210 self.__enable_logging = False
211
212 self.__block_instance_signals = False
213
214 self.__callback_map = {}
215
216
217
218
219
220
221
222 self.__signal_map = {}
223
224
225
226
227 self._current_key = 0
228 self._current_signals = []
229
230
231
232
233
234
235
236
237
238 def trav(cls):
239 """A traversal function to walk through all the classes in
240 the inheritance tree. The return is a list of all the
241 __signals__ dictionaries."""
242 if cls.__dict__.has_key('__signals__'):
243 signal_list = [cls.__signals__]
244 else:
245 signal_list = []
246
247 for base_cls in cls.__bases__:
248 base_list = trav(base_cls)
249 if len(base_list) > 0:
250 signal_list = signal_list + base_list
251
252 return signal_list
253
254
255 for s in trav(self.__class__):
256 for (k, v) in s.items():
257 if self.__signal_map.has_key(k):
258
259 sys.stderr.write("Warning: signal name clash: %s\n" % str(k))
260 self.__signal_map[k] = v
261
262
263
264
265 self._log("registered signals: \n %s\n" %
266 "\n ".join([ "%s: %s" % (k, v) for (k, v)
267 in self.__signal_map.items() ]))
268
269
270 - def connect(self, signal_name, callback):
271 """
272 Connect a callable to a signal_name. The callable will be called
273 with the signal is emitted. The callable must accept the argument
274 types declared in the signals signature.
275
276 returns a unique key that can be passed to disconnect().
277 """
278
279 if signal_name not in self.__signal_map.keys():
280 self._log("Warning: attempt to connect to unknown signal: %s\n" % str(signal_name))
281 return
282
283
284 if signal_name not in self.__callback_map.keys():
285 self.__callback_map[signal_name] = []
286
287 self._current_key += 1
288 self._log("Connecting callback to signal: "
289 "%s with key: %s\n"
290 % (signal_name, str(self._current_key)))
291 self.__callback_map[signal_name].append((self._current_key, callback))
292
293 return self._current_key
294
296 """
297 Disconnect a callback.
298 """
299
300
301 for signal_name in self.__callback_map.keys():
302 for cb in self.__callback_map[signal_name]:
303 (skey, fn) = cb
304 if skey == key:
305
306 self._log("Disconnecting callback from signal"
307 ": %s with key: %s\n" % (signal_name,
308 str(key)))
309 self.__callback_map[signal_name].remove(cb)
310
311
312 - def emit(self, signal_name, args=tuple()):
313 """
314 Emit the signal called signal_name. The args must be a tuple of
315 arguments that match the types declared for the signals signature.
316 """
317
318 if self.__BLOCK_ALL_SIGNALS or \
319 self.__block_instance_signals:
320 return
321
322
323 if signal_name not in self.__signal_map.keys():
324 self._warn("Attempt to emit to unknown signal: %s\n"
325 " from: file: %s\n"
326 " line: %d\n"
327 " func: %s\n"
328 % ((str(signal_name), ) + inspect.stack()[1][1:4]))
329 return
330
331
332
333 if signal_name in self._current_signals:
334 self._warn("Signal recursion blocked. "
335 "Signals was : %s\n"
336 " from: file: %s\n"
337 " line: %d\n"
338 " func: %s\n"
339 % ((str(signal_name), ) + inspect.stack()[1][1:4]))
340 return
341
342 try:
343 self._current_signals.append(signal_name)
344
345
346 if not (isinstance(args, tuple) or args == None):
347 self._warn("Signal emitted with argument that is not a tuple.\n"
348 " emit() takes two arguments, the signal name and a \n"
349 " tuple that contains the arguments that are to be \n"
350 " passed to the callbacks. If you are passing a signal \n"
351 " argument it must be done as a single element tuple \n"
352 " e.g. emit('my-signal', (1, )) \n"
353 " signal was: %s\n"
354 " from: file: %s\n"
355 " line: %d\n"
356 " func: %s\n"
357 % ((str(signal_name), ) + inspect.stack()[1][1:4]))
358 return
359
360
361 arg_types = self.__signal_map[signal_name]
362 if arg_types == None and len(args) > 0:
363 self._warn("Signal emitted with "
364 "wrong number of args: %s\n"
365 " from: file: %s\n"
366 " line: %d\n"
367 " func: %s\n"
368 % ((str(signal_name), ) + inspect.stack()[1][1:4]))
369 return
370
371 if len(args) > 0:
372 if len(args) != len(arg_types):
373 self._warn("Signal emitted with "
374 "wrong number of args: %s\n"
375 " from: file: %s\n"
376 " line: %d\n"
377 " func: %s\n"
378 % ((str(signal_name), ) + inspect.stack()[1][1:4]))
379 return
380
381 if arg_types != None:
382 for i in range(0, len(arg_types)):
383 if not isinstance(args[i], arg_types[i]):
384 self._warn("Signal emitted with "
385 "wrong arg types: %s\n"
386 " from: file: %s\n"
387 " line: %d\n"
388 " func: %s\n"
389 " arg passed was: %s, type of arg passed %s, type should be: %s\n"
390 % ((str(signal_name), ) + inspect.stack()[1][1:4] +\
391 (args[i], repr(type(args[i])), repr(arg_types[i]))))
392 return
393
394 if signal_name in self.__callback_map.keys():
395 self._log("emitting signal: %s\n" % (signal_name, ))
396
397 for (key, fn) in self.__callback_map[signal_name]:
398 self._log("Calling callback with key: %s\n" % (key, ))
399 try:
400 if type(fn) == types.FunctionType or \
401 type(fn) == types.MethodType:
402 fn(*args)
403 else:
404 self._warn("Badly formed entry in callback map.\n")
405 except:
406 self._warn("Exception occurred in callback function.\n"
407 "%s" % ("".join(traceback.format_exception(*sys.exc_info())), ))
408 finally:
409 self._current_signals.remove(signal_name)
410
411
412
413
415 self.__block_instance_signals = True
416
418 self.__block_instance_signals = False
419
420
421
423 self.__enable_logging = False
424
426 self.__enable_logging = True
427
428 - def _log(self, msg):
429 if self.__LOG_ALL or self.__enable_logging:
430 log("%s: %s" % (self.__class__.__name__, str(msg)))
431
433 log("Warning: %s: %s" % (self.__class__.__name__, str(msg)))
434
435
436
437
438
439 @classmethod
442
443 @classmethod
446
447 @classmethod
450