Fixed memory leak on callbacks used on signal connection.

Now using the 'destroyed()' signal the reference is cleaned after source
object destroyed.
This commit is contained in:
Renato Filho 2010-02-19 17:10:24 -03:00
commit 75b7afbd63
11 changed files with 212 additions and 84 deletions

View file

@ -1,3 +1,29 @@
static bool getReceiver(PyObject *callback, QObject **receiver, PyObject **self)
{
if (PyMethod_Check(callback)) {
*self = PyMethod_GET_SELF(callback);
if (SbkQObject_Check(*self))
*receiver = SbkQObject_cptr(*self);
} else if (PyCFunction_Check(callback)) {
*self = PyCFunction_GET_SELF(callback);
if (*self && SbkQObject_Check(*self))
*receiver = SbkQObject_cptr(*self);
} else if (!PyFunction_Check(callback)) {
*receiver = 0;
*self = 0;
qWarning() << "Invalid callback object.";
return false;
}
bool usingGlobalReceiver = !*receiver;
if (usingGlobalReceiver) {
PySide::SignalManager& signalManager = PySide::SignalManager::instance();
*receiver = signalManager.globalReceiver();
}
return usingGlobalReceiver;
}
static bool qobjectConnect(QObject* source, const char* signal, QObject* receiver, const char* slot, Qt::ConnectionType type) static bool qobjectConnect(QObject* source, const char* signal, QObject* receiver, const char* slot, Qt::ConnectionType type)
{ {
if (!PySide::checkSignal(signal)) if (!PySide::checkSignal(signal))
@ -24,25 +50,11 @@ static bool qobjectConnectCallback(QObject* source, const char* signal, PyObject
PySide::SignalManager& signalManager = PySide::SignalManager::instance(); PySide::SignalManager& signalManager = PySide::SignalManager::instance();
// Extract receiver from callback // Extract receiver from callback
bool usingGlobalReceiver;
QObject* receiver = 0; QObject* receiver = 0;
PyObject* self = 0; PyObject* self = 0;
if (PyMethod_Check(callback)) { bool usingGlobalReceiver = getReceiver(callback, &receiver, &self);
self = PyMethod_GET_SELF(callback); if (receiver == 0 and self == 0)
if (SbkQObject_Check(self))
receiver = SbkQObject_cptr(self);
} else if (PyCFunction_Check(callback)) {
self = PyCFunction_GET_SELF(callback);
if (self && SbkQObject_Check(self))
receiver = SbkQObject_cptr(self);
} else if (!PyFunction_Check(callback)) {
qWarning() << "Invalid callback object.";
return false; return false;
}
usingGlobalReceiver = !receiver;
if (usingGlobalReceiver)
receiver = signalManager.globalReceiver();
const QMetaObject* metaObject = receiver->metaObject(); const QMetaObject* metaObject = receiver->metaObject();
const QByteArray callbackSig = PySide::getCallbackSignature(signal, callback, usingGlobalReceiver).toAscii(); const QByteArray callbackSig = PySide::getCallbackSignature(signal, callback, usingGlobalReceiver).toAscii();
@ -66,10 +78,10 @@ static bool qobjectConnectCallback(QObject* source, const char* signal, PyObject
#ifndef AVOID_PROTECTED_HACK #ifndef AVOID_PROTECTED_HACK
source->connectNotify(signal); source->connectNotify(signal);
#else #else
reinterpret_cast<QObjectWrapper*>(source)->connectNotify_protected(signal); reinterpret_cast<QObjectWrapper*>(source)->connectNotify_protected(source, signal);
#endif #endif
if (usingGlobalReceiver) if (usingGlobalReceiver)
signalManager.globalReceiverConnectNotify(slotIndex); signalManager.globalReceiverConnectNotify(source, slotIndex);
return true; return true;
} }
@ -85,17 +97,11 @@ static bool qobjectDisconnectCallback(QObject* source, const char* signal, PyObj
PySide::SignalManager& signalManager = PySide::SignalManager::instance(); PySide::SignalManager& signalManager = PySide::SignalManager::instance();
// Extract receiver from callback // Extract receiver from callback
bool usingGlobalReceiver;
QObject* receiver = 0; QObject* receiver = 0;
PyObject* self; PyObject* self = 0;
if (PyMethod_Check(callback)) { bool usingGlobalReceiver = getReceiver(callback, &receiver, &self);
self = PyMethod_GET_SELF(callback); if (receiver == 0 and self == 0)
if (SbkQObject_Check(self)) return false;
receiver = SbkQObject_cptr(self);
}
usingGlobalReceiver = !receiver;
if (usingGlobalReceiver)
receiver = signalManager.globalReceiver();
const QMetaObject* metaObject = receiver->metaObject(); const QMetaObject* metaObject = receiver->metaObject();
const QByteArray callbackSig = PySide::getCallbackSignature(signal, callback, usingGlobalReceiver).toAscii(); const QByteArray callbackSig = PySide::getCallbackSignature(signal, callback, usingGlobalReceiver).toAscii();
@ -105,7 +111,7 @@ static bool qobjectDisconnectCallback(QObject* source, const char* signal, PyObj
if (QObject::disconnect(source, signal, receiver, qtSlotName.constData())) { if (QObject::disconnect(source, signal, receiver, qtSlotName.constData())) {
if (usingGlobalReceiver) { if (usingGlobalReceiver) {
int slotIndex = metaObject->indexOfSlot(callbackSig.constData()); int slotIndex = metaObject->indexOfSlot(callbackSig.constData());
signalManager.globalReceiverDisconnectNotify(slotIndex); signalManager.globalReceiverDisconnectNotify(source, slotIndex);
} }
return true; return true;
} }

View file

@ -1631,6 +1631,16 @@
</inject-code> </inject-code>
</add-function> </add-function>
<modify-function signature="receivers(const char*) const">
<inject-code class="target" position="beginning">
//Avoid return +1 because SignalManager connect to "destroyed()" signal to control object timelife
int ret = %CPPSELF.%FUNCTION_NAME(%1);
if ((strcmp(%1, SIGNAL(destroyed())) == 0) &amp;&amp; (PySide::SignalManager::instance().hasConnectionWith(%CPPSELF)))
ret--;
%PYARG_0 = %CONVERTTOPYTHON[int](ret);
</inject-code>
</modify-function>
<modify-function signature="sender() const"> <modify-function signature="sender() const">
<modify-argument index="return"> <modify-argument index="return">
<define-ownership owner="target"/> <define-ownership owner="target"/>

View file

@ -58,6 +58,13 @@ static int registerString(const QByteArray& s, QList<QByteArray>* strings)
return idx; return idx;
} }
static void clearItem(QLinkedList<QByteArray> &l, const QByteArray &value)
{
QLinkedList<QByteArray>::iterator i = qFind(l.begin(), l.end(), value);
if (i != l.end())
*i = QByteArray();
}
DynamicQMetaObject::DynamicQMetaObject(const char *className, const QMetaObject* metaObject) DynamicQMetaObject::DynamicQMetaObject(const char *className, const QMetaObject* metaObject)
{ {
d.superdata = metaObject; d.superdata = metaObject;
@ -76,6 +83,15 @@ DynamicQMetaObject::~DynamicQMetaObject()
void DynamicQMetaObject::addSignal(const char* signal) void DynamicQMetaObject::addSignal(const char* signal)
{ {
//search for a empty space
QByteArray blank;
QLinkedList<QByteArray>::iterator i = qFind(m_signals.begin(), m_signals.end(), blank);
if (i != m_signals.end()) {
*i = QByteArray(signal);
updateMetaObject();
return;
}
if (m_signals.size() >= MAX_SIGNALS_COUNT) { if (m_signals.size() >= MAX_SIGNALS_COUNT) {
qWarning() << "Fail to add dynamic signal to QObject. PySide support at most" << MAX_SIGNALS_COUNT << "dynamic signals."; qWarning() << "Fail to add dynamic signal to QObject. PySide support at most" << MAX_SIGNALS_COUNT << "dynamic signals.";
return; return;
@ -87,25 +103,34 @@ void DynamicQMetaObject::addSignal(const char* signal)
void DynamicQMetaObject::addSlot(const char* slot) void DynamicQMetaObject::addSlot(const char* slot)
{ {
m_slots << QByteArray(slot); //search for a empty space
QByteArray blank;
QLinkedList<QByteArray>::iterator i = qFind(m_slots.begin(), m_slots.end(), blank);
if (i != m_slots.end()) {
*i = QByteArray(slot);
} else {
m_slots << QByteArray(slot);
}
updateMetaObject(); updateMetaObject();
} }
void DynamicQMetaObject::removeSlot(uint index) void DynamicQMetaObject::removeSlot(uint index)
{ {
QMetaMethod m = method(index); QMetaMethod m = method(index);
if (m_slots.removeAll(m.signature())) if (m_slots.contains(m.signature())) {
clearItem(m_slots, m.signature());
updateMetaObject(); updateMetaObject();
}
} }
void DynamicQMetaObject::removeSignal(uint index) void DynamicQMetaObject::removeSignal(uint index)
{ {
//Current Qt implementation does not support runtime remove signal //Current Qt implementation does not support runtime remove signal
/*
QMetaMethod m = method(index); QMetaMethod m = method(index);
if (m_signals.removeAll(m.signature())) if (m_signals.contains(m.signature())) {
clearItem(m_signals, m.signature());
updateMetaObject(); updateMetaObject();
*/ }
} }
void DynamicQMetaObject::updateMetaObject() void DynamicQMetaObject::updateMetaObject()

View file

@ -32,94 +32,125 @@
* 02110-1301 USA * 02110-1301 USA
*/ */
#include "globalreceiver.h"
#include <QMetaMethod> #include <QMetaMethod>
#include <QDebug> #include <QDebug>
#include "signalmanager.h" #include <QEvent>
#include <autodecref.h> #include <autodecref.h>
#include <gilstate.h> #include <gilstate.h>
#include "typeresolver.h"
#include "globalreceiver.h"
#include "typeresolver.h"
#include "signalmanager.h"
#include "weakref.h"
#define RECEIVER_DESTROYED_SLOT_NAME "__receiverDestroyed__(QObject*)"
namespace PySide namespace PySide
{ {
class DynamicSlotData class DynamicSlotData
{ {
public: public:
DynamicSlotData(PyObject *callback); DynamicSlotData(int id, PyObject* callback);
void incRef(); void addRef(const QObject* o);
void decRef(); void decRef(const QObject* o);
void clear();
bool hasRefTo(const QObject* o) const;
int refCount() const; int refCount() const;
PyObject *callback() const; int id() const;
PyObject* callback() const;
~DynamicSlotData(); ~DynamicSlotData();
private: private:
int m_refCount; int m_id;
PyObject *m_callback; PyObject* m_callback;
QSet<const QObject*> m_refs;
}; };
} }
using namespace PySide; using namespace PySide;
DynamicSlotData::DynamicSlotData(PyObject *callback) DynamicSlotData::DynamicSlotData(int id, PyObject* callback)
: m_refCount(0) : m_id(id)
{ {
m_callback = callback; m_callback = callback;
Py_INCREF(callback); Py_INCREF(callback);
} }
void DynamicSlotData::incRef() void DynamicSlotData::addRef(const QObject *o)
{ {
m_refCount++; m_refs.insert(o);
} }
void DynamicSlotData::decRef() void DynamicSlotData::decRef(const QObject *o)
{ {
m_refCount--; m_refs.remove(o);
} }
int DynamicSlotData::refCount() const int DynamicSlotData::refCount() const
{ {
return m_refCount; return m_refs.size();
} }
PyObject *DynamicSlotData::callback() const
PyObject* DynamicSlotData::callback() const
{ {
return m_callback; return m_callback;
} }
int DynamicSlotData::id() const
{
return m_id;
}
bool DynamicSlotData::hasRefTo(const QObject *o) const
{
return m_refs.contains(o);
}
void DynamicSlotData::clear()
{
m_refs.clear();
}
DynamicSlotData::~DynamicSlotData() DynamicSlotData::~DynamicSlotData()
{ {
Py_XDECREF(m_callback); Py_DECREF(m_callback);
} }
GlobalReceiver::GlobalReceiver() GlobalReceiver::GlobalReceiver()
: m_metaObject("GlobalReceiver", &QObject::staticMetaObject) : m_metaObject("GlobalReceiver", &QObject::staticMetaObject)
{ {
//slot used to be notifyed of object destrouction
m_metaObject.addSlot(RECEIVER_DESTROYED_SLOT_NAME);
} }
GlobalReceiver::~GlobalReceiver() GlobalReceiver::~GlobalReceiver()
{ {
foreach(DynamicSlotData* data, m_slotReceivers) foreach(DynamicSlotData* data, m_slotReceivers) {
data->clear();
delete data; delete data;
}
} }
void GlobalReceiver::connectNotify(int slotId) void GlobalReceiver::connectNotify(QObject* source, int slotId)
{
if (m_slotReceivers.contains(slotId))
m_slotReceivers[slotId]->incRef();
}
void GlobalReceiver::disconnectNotify(int slotId)
{ {
if (m_slotReceivers.contains(slotId)) { if (m_slotReceivers.contains(slotId)) {
m_slotReceivers[slotId]->addRef(source);
}
}
void GlobalReceiver::disconnectNotify(QObject* source, int slotId)
{
if (m_slotReceivers.contains(slotId)) {
QObject::disconnect(source, SIGNAL(destroyed(QObject*)), this, "1"RECEIVER_DESTROYED_SLOT_NAME);
DynamicSlotData *data = m_slotReceivers[slotId]; DynamicSlotData *data = m_slotReceivers[slotId];
data->decRef(); data->decRef(source);
if (data->refCount() == 0) if (data->refCount() == 0) {
removeSlot(slotId); removeSlot(slotId);
}
} }
} }
@ -133,7 +164,7 @@ void GlobalReceiver::addSlot(const char* slot, PyObject* callback)
m_metaObject.addSlot(slot); m_metaObject.addSlot(slot);
int slotId = m_metaObject.indexOfSlot(slot); int slotId = m_metaObject.indexOfSlot(slot);
if (!m_slotReceivers.contains(slotId)) { if (!m_slotReceivers.contains(slotId)) {
m_slotReceivers[slotId] = new DynamicSlotData(callback); m_slotReceivers[slotId] = new DynamicSlotData(slotId, callback);
} }
bool isShortCircuit = true; bool isShortCircuit = true;
@ -143,6 +174,7 @@ void GlobalReceiver::addSlot(const char* slot, PyObject* callback)
break; break;
} }
} }
if (isShortCircuit) if (isShortCircuit)
m_shortCircuitSlots << slotId; m_shortCircuitSlots << slotId;
@ -151,9 +183,23 @@ void GlobalReceiver::addSlot(const char* slot, PyObject* callback)
void GlobalReceiver::removeSlot(int slotId) void GlobalReceiver::removeSlot(int slotId)
{ {
delete m_slotReceivers.take(slotId); if (m_slotReceivers.contains(slotId)) {
m_metaObject.removeSlot(slotId); delete m_slotReceivers.take(slotId);
m_shortCircuitSlots.remove(slotId); m_metaObject.removeSlot(slotId);
m_shortCircuitSlots.remove(slotId);
}
}
bool GlobalReceiver::hasConnectionWith(const QObject *object)
{
QHash<int, DynamicSlotData*>::iterator i = m_slotReceivers.begin();
while(i != m_slotReceivers.end()) {
if (i.value()->hasRefTo(object)) {
return true;
}
i++;
}
return false;
} }
int GlobalReceiver::qt_metacall(QMetaObject::Call call, int id, void** args) int GlobalReceiver::qt_metacall(QMetaObject::Call call, int id, void** args)
@ -163,6 +209,20 @@ int GlobalReceiver::qt_metacall(QMetaObject::Call call, int id, void** args)
QMetaMethod slot = m_metaObject.method(id); QMetaMethod slot = m_metaObject.method(id);
Q_ASSERT(slot.methodType() == QMetaMethod::Slot); Q_ASSERT(slot.methodType() == QMetaMethod::Slot);
if (strcmp(slot.signature(), RECEIVER_DESTROYED_SLOT_NAME) == 0) {
QObject *arg = *(QObject**)args[1];
QHash<int, DynamicSlotData*>::iterator i = m_slotReceivers.begin();
while(i != m_slotReceivers.end()) {
if (i.value()->hasRefTo(arg)) {
disconnectNotify(arg, i.key());
break;
}
i++;
}
return -1;
}
DynamicSlotData* data = m_slotReceivers.value(id); DynamicSlotData* data = m_slotReceivers.value(id);
if (!data) { if (!data) {
qWarning() << "Unknown global slot, id:" << id; qWarning() << "Unknown global slot, id:" << id;
@ -191,5 +251,6 @@ int GlobalReceiver::qt_metacall(QMetaObject::Call call, int id, void** args)
qWarning() << "Error calling slot" << m_metaObject.method(id).signature(); qWarning() << "Error calling slot" << m_metaObject.method(id).signature();
else else
Py_DECREF(retval); Py_DECREF(retval);
return -1; return -1;
} }

View file

@ -55,8 +55,10 @@ public:
const QMetaObject* metaObject() const; const QMetaObject* metaObject() const;
void addSlot(const char* slot, PyObject* callback); void addSlot(const char* slot, PyObject* callback);
void removeSlot(int slotId); void removeSlot(int slotId);
void connectNotify(int slotId); void connectNotify(QObject* sender, int slotId);
void disconnectNotify(int slotId); void disconnectNotify(QObject* sender, int slotId);
bool hasConnectionWith(const QObject* object);
private: private:
DynamicQMetaObject m_metaObject; DynamicQMetaObject m_metaObject;
QSet<int> m_shortCircuitSlots; QSet<int> m_shortCircuitSlots;

View file

@ -69,7 +69,12 @@ bool PySide::checkSignal(const char* signal)
static QString codeCallbackName(PyObject* callback, const QString& funcName) static QString codeCallbackName(PyObject* callback, const QString& funcName)
{ {
return funcName+QString::number(quint64(callback), 16); if (PyMethod_Check(callback)) {
PyObject *self = PyMethod_GET_SELF(callback);
PyObject *func = PyMethod_GET_FUNCTION(callback);
return funcName + QString::number(quint64(self), 16) + QString::number(quint64(func), 16);
} else
return funcName+QString::number(quint64(callback), 16);
} }
QString PySide::getCallbackSignature(const char* signal, PyObject* callback, bool encodeName) QString PySide::getCallbackSignature(const char* signal, PyObject* callback, bool encodeName)
@ -185,14 +190,14 @@ QObject* SignalManager::globalReceiver()
return &m_d->m_globalReceiver; return &m_d->m_globalReceiver;
} }
void SignalManager::globalReceiverConnectNotify(int slotIndex) void SignalManager::globalReceiverConnectNotify(QObject* source, int slotIndex)
{ {
m_d->m_globalReceiver.connectNotify(slotIndex); m_d->m_globalReceiver.connectNotify(source, slotIndex);
} }
void SignalManager::globalReceiverDisconnectNotify(int slotIndex) void SignalManager::globalReceiverDisconnectNotify(QObject* source, int slotIndex)
{ {
m_d->m_globalReceiver.disconnectNotify(slotIndex); m_d->m_globalReceiver.disconnectNotify(source, slotIndex);
} }
void SignalManager::addGlobalSlot(const char* slot, PyObject* callback) void SignalManager::addGlobalSlot(const char* slot, PyObject* callback)
@ -234,7 +239,6 @@ bool SignalManager::emitSignal(QObject* source, const char* signal, PyObject* ar
return false; return false;
signal++; signal++;
int signalIndex = source->metaObject()->indexOfSignal(signal); int signalIndex = source->metaObject()->indexOfSignal(signal);
if (signalIndex != -1) { if (signalIndex != -1) {
bool isShortCircuit; bool isShortCircuit;
@ -311,3 +315,8 @@ bool SignalManager::registerMetaMethod(QObject* source, const char* signature, Q
} }
return true; return true;
} }
bool SignalManager::hasConnectionWith(const QObject *object)
{
return m_d->m_globalReceiver.hasConnectionWith(object);
}

View file

@ -61,11 +61,14 @@ public:
static int qt_metacall(QObject* object, QMetaObject::Call call, int id, void** args); static int qt_metacall(QObject* object, QMetaObject::Call call, int id, void** args);
void addGlobalSlot(const char* slot, PyObject* callback); void addGlobalSlot(const char* slot, PyObject* callback);
void globalReceiverConnectNotify(int slotIndex); void globalReceiverConnectNotify(QObject *sender, int slotIndex);
void globalReceiverDisconnectNotify(int slotIndex); void globalReceiverDisconnectNotify(QObject *sender, int slotIndex);
// Used to register a new signal/slot on QMetaobjc of source. // Used to register a new signal/slot on QMetaobject of source.
static bool registerMetaMethod(QObject* source, const char* signature, QMetaMethod::MethodType type); static bool registerMetaMethod(QObject* source, const char* signature, QMetaMethod::MethodType type);
// Used to discovery if SignalManager was connected with object "destroyed()" signal.
bool hasConnectionWith(const QObject *object);
private: private:
struct SignalManagerPrivate; struct SignalManagerPrivate;
SignalManagerPrivate* m_d; SignalManagerPrivate* m_d;

View file

@ -23,7 +23,7 @@ class QObjectReceivers(unittest.TestCase):
self.assertEqual(obj.receivers(SIGNAL("destroyed()")), 0) self.assertEqual(obj.receivers(SIGNAL("destroyed()")), 0)
QObject.connect(obj, SIGNAL("destroyed()"), self.cb) QObject.connect(obj, SIGNAL("destroyed()"), self.cb)
self.assertEqual(obj.receivers(SIGNAL("destroyed()")), 1) self.assertEqual(obj.receivers(SIGNAL("destroyed()")), 0)
def testQThreadReceiversExtern(self): def testQThreadReceiversExtern(self):
#QThread.receivers() - Inherited protected method #QThread.receivers() - Inherited protected method
@ -31,7 +31,7 @@ class QObjectReceivers(unittest.TestCase):
obj = QThread() obj = QThread()
self.assertEqual(obj.receivers(SIGNAL('destroyed()')), 0) self.assertEqual(obj.receivers(SIGNAL('destroyed()')), 0)
QObject.connect(obj, SIGNAL("destroyed()"), self.cb) QObject.connect(obj, SIGNAL("destroyed()"), self.cb)
self.assertEqual(obj.receivers(SIGNAL("destroyed()")), 1) self.assertEqual(obj.receivers(SIGNAL("destroyed()")), 0)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -10,7 +10,7 @@ from helper import BasicPySlotCase, UsesQCoreApplication
from helper.decorators import requires from helper.decorators import requires
def random_gen(count=100, largest=99, lowest=0): def random_gen(count=50, largest=49, lowest=0):
for i in range(count): for i in range(count):
yield random.randint(lowest, largest) yield random.randint(lowest, largest)
@ -50,7 +50,7 @@ class PythonMultipleSlots(UsesQCoreApplication, MultipleSignalConnections):
class Dummy(QObject): class Dummy(QObject):
pass pass
for test in random_gen(30): for test in random_gen(20):
sender = Dummy() sender = Dummy()
receivers = [BasicPySlotCase() for x in range(10)] receivers = [BasicPySlotCase() for x in range(10)]
self.run_many(sender, 'foobar', partial(sender.emit, self.run_many(sender, 'foobar', partial(sender.emit,

View file

@ -50,6 +50,18 @@ class PythonSigSlot(unittest.TestCase):
self.assert_(self.called) self.assert_(self.called)
def testDisconnect(self):
obj1 = Dummy()
QObject.connect(obj1, SIGNAL('foo(int)'), self.callback)
QObject.disconnect(obj1, SIGNAL('foo(int)'), self.callback)
self.args = (42, )
obj1.emit(SIGNAL('foo(int)'), *self.args)
self.assert_(not self.called)
@requires('PySide.QtGui') @requires('PySide.QtGui')
class SpinBoxPySignal(UsesQApplication): class SpinBoxPySignal(UsesQApplication):
"""Tests the connection of python signals to QSpinBox qt slots.""" """Tests the connection of python signals to QSpinBox qt slots."""

View file

@ -30,9 +30,9 @@ class TestQObjectReceivers(unittest.TestCase):
sender = QObject() sender = QObject()
receiver = QObject() receiver = QObject()
sender.connect(sender, SIGNAL("destroyed()"), cute_slot) sender.connect(sender, SIGNAL("destroyed()"), cute_slot)
self.assertEqual(sender.receivers(SIGNAL("destroyed( )")), 1) self.assertEqual(sender.receivers(SIGNAL("destroyed( )")), 0)
sender.connect(sender, SIGNAL("destroyed()"), receiver, SLOT("deleteLater()")) sender.connect(sender, SIGNAL("destroyed()"), receiver, SLOT("deleteLater()"))
self.assertEqual(sender.receivers(SIGNAL("destroyed()")), 2) self.assertEqual(sender.receivers(SIGNAL("destroyed()")), 1)
del sender del sender
del receiver del receiver