2005-04-01 Richard Taylor <rjt-gramps@thegrindstone.me.uk>
* src/GrampsDBCallback.py: added support for disconnecting callbacks, better test code and lots of logging. Improved the comments. svn: r4298
This commit is contained in:
parent
d52de130ff
commit
67168c3ab5
@ -1,3 +1,7 @@
|
|||||||
|
2005-04-01 Richard Taylor <rjt-gramps@thegrindstone.me.uk>
|
||||||
|
* src/GrampsDBCallback.py: added support for disconnecting callbacks,
|
||||||
|
better test code and lots of logging. Improved the comments.
|
||||||
|
|
||||||
2005-04-04 Alex Roitman <shura@gramps-project.org>
|
2005-04-04 Alex Roitman <shura@gramps-project.org>
|
||||||
* src/Marriage.py: proper window management on delete event.
|
* src/Marriage.py: proper window management on delete event.
|
||||||
* src/RelLib.py (Event.are_equal): Correction.
|
* src/RelLib.py (Event.are_equal): Correction.
|
||||||
|
@ -20,9 +20,26 @@
|
|||||||
|
|
||||||
# $Id$
|
# $Id$
|
||||||
|
|
||||||
|
"""
|
||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
Gramps is devided into two parts. The database code, that does not
|
||||||
|
require any particular GUI libraries, and the gtk-based UI code
|
||||||
|
that requires gtk and gnome libraries. The gtk-based code can use
|
||||||
|
the gobject signal support to manage callback signals but the database
|
||||||
|
code can not.
|
||||||
|
|
||||||
|
The module provides a subset of the signal mechanisms that are available
|
||||||
|
from the gobject framework. It enables the database code to use signals
|
||||||
|
to communicate events to any callback methods in either the database code
|
||||||
|
or the UI code.
|
||||||
|
"""
|
||||||
import types
|
import types
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
log = sys.stderr.write
|
||||||
|
|
||||||
#-------------------------------------------------------------------------
|
#-------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# Callback signal support for non-gtk parts of Gramps
|
# Callback signal support for non-gtk parts of Gramps
|
||||||
@ -33,17 +50,117 @@ class GrampsDBCallback(object):
|
|||||||
"""
|
"""
|
||||||
Callback and signal support for non-gtk parts of gramps.
|
Callback and signal support for non-gtk parts of gramps.
|
||||||
|
|
||||||
Classes that want to emit signals need to inherit from this
|
Declaring signals
|
||||||
class and call its __init__ method. They then need to declare
|
=================
|
||||||
the signals that they can emit and the types of their
|
|
||||||
arguments.
|
Classes that want to emit signals need to inherit from the
|
||||||
|
GrampsDBCallback class and ensure that its __init__ method
|
||||||
|
is called. They then need to declare the signals that they
|
||||||
|
can emit and the types of each callbacks arguments.
|
||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
|
|
||||||
class TestSignals(GrampsDBCallback):
|
class TestSignals(GrampsDBCallback):
|
||||||
|
|
||||||
__signals__ = {
|
__signals__ = {
|
||||||
'test-signal' : (int,)
|
'test-signal' : (int,),
|
||||||
|
'test-noarg' : None
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
GrampsDBCallback.__init__(self)
|
||||||
|
|
||||||
|
The type signature is a tuple of types or classes. The type
|
||||||
|
checking code uses the isinstance method to check that the
|
||||||
|
argument passed to the emit call is an instance of the type
|
||||||
|
in the signature declaration.
|
||||||
|
|
||||||
|
If the signal does not have an argument use None as the
|
||||||
|
signature.
|
||||||
|
|
||||||
|
The signals will be inherited by any subclasses. Duplicate
|
||||||
|
signal names in subclasses are not alowed.
|
||||||
|
|
||||||
|
|
||||||
|
Emitting signals
|
||||||
|
================
|
||||||
|
|
||||||
|
Signals are emitted using the emit method.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
def emit_signal(self):
|
||||||
|
self.emit('test-signal',(1,))
|
||||||
|
|
||||||
|
The parameters are passed as a tuple so a single parameter
|
||||||
|
must be passed as a 1 element tuple.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Connecting callbacks to signals
|
||||||
|
==============================
|
||||||
|
|
||||||
|
Attaching a callback to the signals is similar to the gtk
|
||||||
|
connect methods. e.g.
|
||||||
|
|
||||||
|
# connect to a function.
|
||||||
|
def fn(i):
|
||||||
|
print "got signal value = ", i
|
||||||
|
|
||||||
|
t = TestSignals()
|
||||||
|
t.connect('test-signal', fn)
|
||||||
|
|
||||||
|
# connect to a bound method
|
||||||
|
class C(object):
|
||||||
|
|
||||||
|
def cb_func(self, i):
|
||||||
|
print "got class signal = ", 1
|
||||||
|
|
||||||
|
r = R()
|
||||||
|
t.connect('test-signal', r.cb_func)
|
||||||
|
|
||||||
|
|
||||||
|
Disconnecting callbacks
|
||||||
|
=======================
|
||||||
|
|
||||||
|
If you want to disconnect a callback from a signals you must remember the
|
||||||
|
key returned from the connect call. This key can be passed to the disconnect
|
||||||
|
method to remove the callback from the signals callback list.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
t = TestSignals()
|
||||||
|
|
||||||
|
# connect to a bound method
|
||||||
|
class C(object):
|
||||||
|
|
||||||
|
def cb_func(self, i):
|
||||||
|
print "got class signal = ", 1
|
||||||
|
|
||||||
|
r = R()
|
||||||
|
key = t.connect('test-signal', r.cb_func)
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
t.disconnect(key)
|
||||||
|
|
||||||
|
|
||||||
|
Stopping and starting signals
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Signals can be blocked on a per instance bassis or they can be blocked
|
||||||
|
for all instances of the GrampsDBCallback class. disable_signals() can
|
||||||
|
be used to block the signals for a single instance and disable_all_signals()
|
||||||
|
can be used to block signals for the class:
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
|
||||||
|
class TestSignals(GrampsDBCallback):
|
||||||
|
|
||||||
|
__signals__ = {
|
||||||
|
'test-signal' : (int,),
|
||||||
|
'test-noarg' : None
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -52,47 +169,69 @@ class GrampsDBCallback(object):
|
|||||||
def emit_signal(self):
|
def emit_signal(self):
|
||||||
self.emit('test-signal',(1,))
|
self.emit('test-signal',(1,))
|
||||||
|
|
||||||
The signals will be inherited by any subclasses.
|
t = TestSignals()
|
||||||
|
|
||||||
Attaching a callback to the signals is similar to the gtk
|
# block signals from instance t
|
||||||
connect methods. e.g.
|
t.disable_signals()
|
||||||
|
|
||||||
class C(object):
|
...
|
||||||
|
|
||||||
|
# unblock
|
||||||
|
t.enable_signals()
|
||||||
|
|
||||||
def cb_func(self, i):
|
# block all signals
|
||||||
print "got class signal = ", 1
|
GrampsDBCallback.disable_all_signals()
|
||||||
|
|
||||||
def fn(i):
|
...
|
||||||
print "got signal value = ", i
|
|
||||||
|
# unblock all signals
|
||||||
|
GrampsDBCallback.enable_all_signals()
|
||||||
|
|
||||||
t = TestSignals()
|
|
||||||
|
|
||||||
# connect to a function.
|
Any signals emited whilst signals are blocked will be lost.
|
||||||
t.connect('test-signal', fn)
|
|
||||||
|
|
||||||
t.emit_signal()
|
Debugging signal callbacks
|
||||||
|
==========================
|
||||||
|
|
||||||
r = R()
|
|
||||||
|
To help with debugging the signals and callbacks you can turn on
|
||||||
|
lots of logging information. To switch on logging for a single
|
||||||
|
instance call self.enable_logging(), to switch it off again call
|
||||||
|
self.disable_logging(). To switch on logging for all instance
|
||||||
|
you can toggle GrampsDBCallback.__LOG_ALL to True.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# If this True no signals will be emitted from any instance of
|
# If this True no signals will be emitted from any instance of
|
||||||
# any class derived from this class. This should be toggled using
|
# any class derived from this class. This should be toggled using
|
||||||
# the class methods, dissable_all_signals() and enable_all_signals().
|
# the class methods, dissable_all_signals() and enable_all_signals().
|
||||||
__BLOCK_ALL_SIGNALS = False
|
__BLOCK_ALL_SIGNALS = False
|
||||||
|
|
||||||
|
|
||||||
|
# If this is True logging will be turned on for all instances
|
||||||
|
# whether or not instance based logging is enabled.
|
||||||
|
__LOG_ALL = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.__enable_logging = False # controls whether lots of debug
|
||||||
|
# information will be produced.
|
||||||
self.__block_instance_signals = False # controls the blocking of
|
self.__block_instance_signals = False # controls the blocking of
|
||||||
# signals from this instance
|
# signals from this instance
|
||||||
self.__callback_map = {} # dictionary containing all the connected
|
self.__callback_map = {} # dictionary containing all the connected
|
||||||
# callback functions. The keys are the
|
# callback functions. The keys are the
|
||||||
# signal names and the values are the
|
# signal names and the values are tuples
|
||||||
# bound methods that will be called when
|
# of the form (key,bound_method), where
|
||||||
# the signal is emitted
|
# the key is unique within the instance
|
||||||
|
# and the bound_method is the callback
|
||||||
|
# that will be called when the signal is
|
||||||
|
# emitted
|
||||||
self.__signal_map = {} # dictionary contains all the signals that
|
self.__signal_map = {} # dictionary contains all the signals that
|
||||||
# this instance can emit. The keys are the
|
# this instance can emit. The keys are the
|
||||||
# signal names and the values are tuples
|
# signal names and the values are tuples
|
||||||
# containing the list of types of the arguments
|
# containing the list of types of the arguments
|
||||||
# that the callback methods must accept.
|
# that the callback methods must accept.
|
||||||
|
self._current_key = 0 # counter to give a unique key to each callback.
|
||||||
|
|
||||||
# To speed up the signal type checking the signals declared by
|
# To speed up the signal type checking the signals declared by
|
||||||
# each of the classes in the inheritance tree of this instance
|
# each of the classes in the inheritance tree of this instance
|
||||||
@ -125,20 +264,59 @@ class GrampsDBCallback(object):
|
|||||||
|
|
||||||
# self.__signal_map now contains the connonical list
|
# self.__signal_map now contains the connonical list
|
||||||
# of signals that this instance can emit.
|
# of signals that this instance can emit.
|
||||||
|
|
||||||
|
self._log("registed signals: \n %s\n" %
|
||||||
|
"\n ".join([ "%s: %s" % (k,v) for (k,v)
|
||||||
|
in self.__signal_map.items() ]))
|
||||||
|
|
||||||
|
|
||||||
def connect(self, signal_name, callback):
|
def connect(self, signal_name, callback):
|
||||||
|
"""
|
||||||
|
Connect a callable to a signal_name. The callable will be called
|
||||||
|
with the signal is emitted. The callable must accept the argument
|
||||||
|
types declared in the signals signature.
|
||||||
|
|
||||||
|
returns a unique key that can be passed to disconnect().
|
||||||
|
"""
|
||||||
# Check that signal exists.
|
# Check that signal exists.
|
||||||
if signal_name not in self.__signal_map.keys():
|
if signal_name not in self.__signal_map.keys():
|
||||||
sys.stderr.write("Warning: attempt to connect to unknown signal: %s\n" % str(signal_name))
|
self._log("Warning: attempt to connect to unknown signal: %s\n" % str(signal_name))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Add callable to callback_map
|
# Add callable to callback_map
|
||||||
if signal_name not in self.__callback_map.keys():
|
if signal_name not in self.__callback_map.keys():
|
||||||
self.__callback_map[signal_name] = []
|
self.__callback_map[signal_name] = []
|
||||||
self.__callback_map[signal_name].append(callback)
|
|
||||||
|
|
||||||
|
self._current_key += 1
|
||||||
|
self._log("Connecting callback to signal: "
|
||||||
|
"%s with key: %s\n"
|
||||||
|
% (signal_name,str(self._current_key)))
|
||||||
|
self.__callback_map[signal_name].append((self._current_key,callback))
|
||||||
|
|
||||||
|
return self._current_key
|
||||||
|
|
||||||
|
def disconnect(self,key):
|
||||||
|
"""
|
||||||
|
Disconnect a callback.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Find the key in the callback map.
|
||||||
|
for signal_name in self.__callback_map.keys():
|
||||||
|
for cb in self.__callback_map[signal_name]:
|
||||||
|
(skey,fn) = cb
|
||||||
|
if skey == key:
|
||||||
|
# delete the callback from the map.
|
||||||
|
self._log("Disconnecting callback from signal"
|
||||||
|
": %s with key: %s\n" % (signal_name,
|
||||||
|
str(key)))
|
||||||
|
self.__callback_map[signal_name].remove(cb)
|
||||||
|
|
||||||
|
|
||||||
def emit(self, signal_name, args=tuple()):
|
def emit(self, signal_name, args=tuple()):
|
||||||
|
"""
|
||||||
|
Emit the signal called signal_name. The args must be a tuple of
|
||||||
|
arguments that match the types declared for the signals signature.
|
||||||
|
"""
|
||||||
# Check that signals are not blocked
|
# Check that signals are not blocked
|
||||||
if GrampsDBCallback.__BLOCK_ALL_SIGNALS or \
|
if GrampsDBCallback.__BLOCK_ALL_SIGNALS or \
|
||||||
self.__block_instance_signals:
|
self.__block_instance_signals:
|
||||||
@ -146,45 +324,47 @@ class GrampsDBCallback(object):
|
|||||||
|
|
||||||
# Check signal exists
|
# Check signal exists
|
||||||
if signal_name not in self.__signal_map.keys():
|
if signal_name not in self.__signal_map.keys():
|
||||||
sys.stderr.write("Warning: attempt to emit to unknown signal: %s\n"
|
self._log("Warning: attempt to emit to unknown signal: %s\n"
|
||||||
% str(signal_name))
|
% str(signal_name))
|
||||||
return
|
return
|
||||||
|
|
||||||
# type check arguments
|
# type check arguments
|
||||||
arg_types = self.__signal_map[signal_name]
|
arg_types = self.__signal_map[signal_name]
|
||||||
if arg_types == None and len(args) > 0:
|
if arg_types == None and len(args) > 0:
|
||||||
sys.stderr.write("Warning: signal emitted with "\
|
self._log("Warning: signal emitted with "\
|
||||||
"wrong number of args: %s\n" % str(signal_name))
|
"wrong number of args: %s\n" % str(signal_name))
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(args) > 0:
|
if len(args) > 0:
|
||||||
if len(args) != len(arg_types):
|
if len(args) != len(arg_types):
|
||||||
sys.stderr.write("Warning: signal emitted with "\
|
self._log("Warning: signal emitted with "\
|
||||||
"wrong number of args: %s\n" % str(signal_name))
|
"wrong number of args: %s\n" % str(signal_name))
|
||||||
return
|
return
|
||||||
|
|
||||||
if arg_types != None:
|
if arg_types != None:
|
||||||
for i in range(0,len(arg_types)):
|
for i in range(0,len(arg_types)):
|
||||||
if not isinstance(args[i],arg_types[i]):
|
if not isinstance(args[i],arg_types[i]):
|
||||||
sys.stderr.write("Warning: signal emitted with "\
|
self._log("Warning: signal emitted with "\
|
||||||
"wrong arg types: %s\n" % (str(signal_name),))
|
"wrong arg types: %s\n" % (str(signal_name),))
|
||||||
sys.stderr.write(" arg passed was: %s, type should be: %s\n"
|
self._log(" arg passed was: %s, type should be: %s\n"
|
||||||
% (args[i],repr(arg_types[i])))
|
% (args[i],repr(arg_types[i])))
|
||||||
return
|
return
|
||||||
|
|
||||||
if signal_name in self.__callback_map.keys():
|
if signal_name in self.__callback_map.keys():
|
||||||
|
self._log("emmitting signal: %s\n" % (signal_name,))
|
||||||
# Don't bother if there are no callbacks.
|
# Don't bother if there are no callbacks.
|
||||||
for cb in self.__callback_map[signal_name]:
|
for (key,fn) in self.__callback_map[signal_name]:
|
||||||
|
self._log("Calling callback with key: %s\n" % (key,))
|
||||||
try:
|
try:
|
||||||
if type(cb) == tuple: # call class method
|
if type(fn) == tuple: # call class method
|
||||||
cb[0](cb[1],*args)
|
cb[0](fn[1],*args)
|
||||||
elif type(cb) == types.FunctionType or \
|
elif type(fn) == types.FunctionType or \
|
||||||
type(cb) == types.MethodType: # call func
|
type(fn) == types.MethodType: # call func
|
||||||
cb(*args)
|
fn(*args)
|
||||||
else:
|
else:
|
||||||
sys.stderr.write("Warning: badly formed entry in callback map.\n")
|
self._log("Warning: badly formed entry in callback map.\n")
|
||||||
except:
|
except:
|
||||||
sys.stderr.write("Warning: exception occured in callback function.\n")
|
self._log("Warning: exception occured in callback function.\n")
|
||||||
|
|
||||||
#
|
#
|
||||||
# instance signals control methods
|
# instance signals control methods
|
||||||
@ -194,7 +374,20 @@ class GrampsDBCallback(object):
|
|||||||
|
|
||||||
def enable_signals(self):
|
def enable_signals(self):
|
||||||
self.__block_instance_signals = False
|
self.__block_instance_signals = False
|
||||||
|
|
||||||
|
|
||||||
|
# logging methods
|
||||||
|
|
||||||
|
def disable_logging(self):
|
||||||
|
self.__enable_logging = False
|
||||||
|
|
||||||
|
def enable_logging(self):
|
||||||
|
self.__enable_logging = True
|
||||||
|
|
||||||
|
def _log(self,msg):
|
||||||
|
if GrampsDBCallback.__LOG_ALL or self.__enable_logging:
|
||||||
|
log("%s: %s" % (self.__class__.__name__, str(msg)))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Class methods
|
# Class methods
|
||||||
#
|
#
|
||||||
@ -241,6 +434,33 @@ if __name__ == "__main__":
|
|||||||
assert len(rl) == 1, "No signal emitted"
|
assert len(rl) == 1, "No signal emitted"
|
||||||
assert rl[0] == 1, "Wrong argument recieved"
|
assert rl[0] == 1, "Wrong argument recieved"
|
||||||
|
|
||||||
|
def test_disconnect(self):
|
||||||
|
|
||||||
|
class TestSignals(GrampsDBCallback):
|
||||||
|
|
||||||
|
__signals__ = {
|
||||||
|
'test-signal' : (int,)
|
||||||
|
}
|
||||||
|
|
||||||
|
rl = []
|
||||||
|
def fn(i,r=rl):
|
||||||
|
rl.append(i)
|
||||||
|
|
||||||
|
t = TestSignals()
|
||||||
|
key = t.connect('test-signal',fn)
|
||||||
|
t.emit('test-signal',(1,))
|
||||||
|
|
||||||
|
|
||||||
|
assert len(rl) == 1, "No signal emitted"
|
||||||
|
assert rl[0] == 1, "Wrong argument recieved"
|
||||||
|
|
||||||
|
t.disconnect(key)
|
||||||
|
|
||||||
|
t.emit('test-signal',(1,))
|
||||||
|
|
||||||
|
assert len(rl) == 1, "Callback not disconnected"
|
||||||
|
assert rl[0] == 1, "Callback not disconnected"
|
||||||
|
|
||||||
def test_noargs(self):
|
def test_noargs(self):
|
||||||
|
|
||||||
class TestSignals(GrampsDBCallback):
|
class TestSignals(GrampsDBCallback):
|
||||||
@ -261,6 +481,17 @@ if __name__ == "__main__":
|
|||||||
assert len(rl) == 1, "No signal emitted"
|
assert len(rl) == 1, "No signal emitted"
|
||||||
assert rl[0] == 1, "Wrong argument recieved"
|
assert rl[0] == 1, "Wrong argument recieved"
|
||||||
|
|
||||||
|
def test_no_callback(self):
|
||||||
|
|
||||||
|
class TestSignals(GrampsDBCallback):
|
||||||
|
|
||||||
|
__signals__ = {
|
||||||
|
'test-noargs' : None
|
||||||
|
}
|
||||||
|
|
||||||
|
t = TestSignals()
|
||||||
|
t.emit('test-noargs')
|
||||||
|
|
||||||
def test_subclassing(self):
|
def test_subclassing(self):
|
||||||
|
|
||||||
class TestSignals(GrampsDBCallback):
|
class TestSignals(GrampsDBCallback):
|
||||||
@ -367,12 +598,11 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# This should fail because the type of arg1 is wrong
|
# This should fail because the type of arg1 is wrong
|
||||||
res=[]
|
res=[]
|
||||||
class C:
|
def fn(s,r=res):
|
||||||
def write(self,s,r=res):
|
res.append(s)
|
||||||
res.append(s)
|
t._log = fn
|
||||||
sys.stderr = C()
|
|
||||||
t.connect('test-lots',fn2), t.emit('test-lots',('a','a',[1,2],t,1.2))
|
t.connect('test-lots',fn2), t.emit('test-lots',('a','a',[1,2],t,1.2))
|
||||||
assert res[0][0:8] == "Warning:", "Type error not detected"
|
assert res[1][0:8] == "Warning:", "Type error not detected"
|
||||||
|
|
||||||
|
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Loading…
Reference in New Issue
Block a user