Package gen :: Package utils :: Module callback
[frames] | no frames]

Source Code for Module gen.utils.callback

  1  # 
  2  # Gramps - a GTK+/GNOME based genealogy program 
  3  # 
  4  # Copyright (C) 2000-2005  Donald N. Allingham 
  5  # 
  6  # This program is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful,  
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the Free Software 
 18  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 19  # 
 20   
 21  # $Id: callback.py 10086 2008-02-21 04:58:56Z dallingham $ 
 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 # Callback signal support for non-gtk parts of Gramps 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 # If this True no signals will be emitted from any instance of 204 # any class derived from this class. This should be toggled using 205 # the class methods, dissable_all_signals() and enable_all_signals(). 206 __BLOCK_ALL_SIGNALS = False 207 __LOG_ALL = False 208
209 - def __init__(self):
210 self.__enable_logging = False # controls whether lots of debug 211 # information will be produced. 212 self.__block_instance_signals = False # controls the blocking of 213 # signals from this instance 214 self.__callback_map = {} # dictionary containing all the connected 215 # callback functions. The keys are the 216 # signal names and the values are tuples 217 # of the form (key, bound_method), where 218 # the key is unique within the instance 219 # and the bound_method is the callback 220 # that will be called when the signal is 221 # emitted 222 self.__signal_map = {} # dictionary contains all the signals that 223 # this instance can emit. The keys are the 224 # signal names and the values are tuples 225 # containing the list of types of the arguments 226 # that the callback methods must accept. 227 self._current_key = 0 # counter to give a unique key to each callback 228 self._current_signals = [] # list of all the signals that are currently 229 # being emitted by this instance. This is 230 # used to prevent recursive emittion of the 231 # same signal. 232 233 # To speed up the signal type checking the signals declared by 234 # each of the classes in the inheritance tree of this instance 235 # are consolidated into a single dictionary. 236 # The signals can't change so we only need to do this once. 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 # Build a signal dict from the list of signal dicts 255 for s in trav(self.__class__): 256 for (k, v) in s.items(): 257 if self.__signal_map.has_key(k): 258 # signal name clash 259 sys.stderr.write("Warning: signal name clash: %s\n" % str(k)) 260 self.__signal_map[k] = v 261 262 # self.__signal_map now contains the connonical list 263 # of signals that this instance can emit. 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 # Check that signal exists. 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 # Add callable to callback_map 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
295 - def disconnect(self, key):
296 """ 297 Disconnect a callback. 298 """ 299 300 # Find the key in the callback map. 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 # delete the callback from the map. 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 # Check that signals are not blocked 318 if self.__BLOCK_ALL_SIGNALS or \ 319 self.__block_instance_signals: 320 return 321 322 # Check signal exists 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 # check that the signal is not already being emitted. This prevents 332 # against recursive signal emissions. 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 # check that args is a tuple. This is a common programming error. 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 # type check arguments 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 # Don't bother if there are no callbacks. 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: # call func 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 # instance signals control methods 413 #
414 - def disable_signals(self):
415 self.__block_instance_signals = True
416
417 - def enable_signals(self):
418 self.__block_instance_signals = False
419 420 # logging methods 421
422 - def disable_logging(self):
423 self.__enable_logging = False
424
425 - def enable_logging(self):
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
432 - def _warn(self, msg):
433 log("Warning: %s: %s" % (self.__class__.__name__, str(msg)))
434 435 # 436 # Class methods 437 # 438 439 @classmethod
440 - def log_all(cls, enable):
441 cls.__LOG_ALL = enable
442 443 @classmethod
444 - def disable_all_signals(cls):
445 cls.__BLOCK_ALL_SIGNALS = True
446 447 @classmethod
448 - def enable_all_signals(cls):
449 cls.__BLOCK_ALL_SIGNALS = False
450