[PyQt] Weird behaviour of destroyed() slots

Lorenzo Mancini lmancini at develer.com
Mon Jan 26 17:07:15 GMT 2009


Phil Thompson wrote:
> On Thu, 22 Jan 2009 15:56:28 +0100, Lorenzo Mancini <lmancini at develer.com>
> wrote:
>> Phil Thompson wrote:
>>> On Sat, 26 Jul 2008 21:07:09 +0200, Giovanni Bajo <rasky at develer.com>
>>> wrote:
>>>> On Sat, 2008-07-26 at 17:10 +0100, Phil Thompson wrote:
>>>>> On Wed, 23 Jul 2008 15:42:18 +0200, Giovanni Bajo <rasky at develer.com>
>>>>> wrote:
>>>>>> Hi Phil:
>>>>>>
>>>>>>
> =============================================================================
>>>>>> import sip
>>>>>> from PyQt4.Qt import *
>>>>>>
>>>>>> called = []
>>>>>>
>>>>>> class Core(QObject):
>>>>>>      def __init__(self, parent=None):
>>>>>>          QWidget.__init__(self, parent)
>>>>>>          QObject.connect(self, SIGNAL("destroyed()"), self.callback)
>>>>>>          QObject.connect(self, SIGNAL("destroyed()"), lambda: 
>>>>>> self.callback())
>>>>>>      def callback(self):
>>>>>>          called.append("done")
>>>>>>
>>>>>> app = QApplication([])
>>>>>> core = Core(app)
>>>>>> sip.delete(core)
>>>>>>
>>>>>> assert len(called) == 2, called
>>>>>>
> =============================================================================
>>>>>> Traceback (most recent call last):
>>>>>>    File "bugpyqt.py", line 18, in <module>
>>>>>>      assert len(called) == 2, called
>>>>>> AssertionError: ['done']
>>>>>>
>>>>>> The slot with "lambda" is called, but the other one is not.
>>>>> The reason is that PyQt knows that self is being destroyed and
>>>>> self.callback may be a wrapped C++ method (although it isn't in this
>>>> case)
>>>>> so it won't invoke it.
>>>> Why does it refuse to invoke a wrapped C++ method? Even if self is
> being
>>>> destroyed (that is: within its C++ destructor) is still valid to invoke
>>>> methods AFAICT. After all, it's something that's perfectly doable in
> C++
>>>> as well. Or not?
>>> I had second thoughts after my first response. A wrapped C++ method will
>>> check to see if the C++ instance has been destroyed anyway, so there is
>>> no
>>> need to check it here as well.
>>>
>>> However, a wrapped C++ method still won't work in exactly the same way
> as
>>> a
>>> C++ application because the "C++ instance has been destroyed" flag has
> to
>>> be set before the destroyed() signal has been emitted. But it's ok for
>>> pure
>>> Python methods.
>> Hi Phil, it seems that the commit originated from this discussion (grep 
>> "2008/07/26" in the sip changelog, just before 4.7.7 was released) 
>> introduced a bug in how PyQt manages autodisconnection when delete is 
>> called on a connected C++ object.  Please consider the following snippet:
>>
>> ***********************************************************
>> import sip
>> from PyQt4.Qt import *
>>
>> class Receiver(QObject):
>>      def myslot(self):
>>          print self.objectName()
>>
>> emitter = QObject(None)
>> receiver = Receiver(None)
>>
>> QObject.connect(emitter, SIGNAL("TEST()"), receiver.myslot)
>> emitter.emit(SIGNAL("TEST()")) # myslot gets called
>> sip.delete(receiver)           # autodisconnection expected
>>
>> # PyQt 4.4.2, SIP 4.7.6 - Disconnected as expected
>> # PyQt 4.4.4, SIP 4.7.8/4.7.9 - Still connected
>>
>> emitter.emit(SIGNAL("TEST()"))
>> ***********************************************************
>>
>> One would expect the runtime to break the connection after the 
>> sip.delete() invocation, instead the connection is still in place with 
>> recent versions of sip, so when the "TEST()" signal is emitted for the 
>> second time, execution aborts with the feared "RuntimeError: underlying 
>> C/C++ object has been deleted".
>>
>>
>> If you test the same behaviour with a wrapped C++ slot, you'll see that 
>> sip breaks the connection as expected.
>> ***********************************************************
>> import sip
>> from PyQt4.Qt import *
>>
>> app = QApplication([])
>> emitter = QObject(None)
>> receiver = QWidget(None)
>> QObject.connect(emitter, SIGNAL("TEST()"), receiver, SLOT("show()"))
>> emitter.emit(SIGNAL("TEST()"))
>> sip.delete(receiver)
>> emitter.emit(SIGNAL("TEST()"))
>> ***********************************************************
> 
> I think the current behaviour is correct and consistent with how PyQt
> handles disappearing C++ objects.
> 
> In your first case you are connecting to a Python callable which is still a
> valid reference even though the underlying QObject has gone. PyQt will only
> complain about that if you try to use it, as you are doing by calling
> objectName().
> 
> If, instead, you had "del receiver" rather than "sip.delete(receiver)" then
> the autodisconnect would happen.
> 
> If, as you show in the second example, you had connected to a Qt slot (eg.
> by decorating myslot() with pyqtSignature) then you would also get the
> autodisconnect.

You are right, decorating with @pyqtSignature and modifying the signal 
and slot signatures accordingly, I get the desired behaviour.  Just for 
reference:

***********************************************************
import sip
from PyQt4.Qt import *

class Receiver(QObject):
     @pyqtSignature("const QString&")
     def myslot(self, arg):
         print arg + self.objectName()

emitter = QObject(None)
receiver = Receiver(None)

QObject.connect(emitter, SIGNAL("TEST(const QString&)"), receiver, 
SLOT("myslot(const QString&)"))
emitter.emit(SIGNAL("TEST(const QString&)"), "1") # myslot gets called
sip.delete(receiver) # autodisconnection expected

emitter.emit(SIGNAL("TEST(const QString&)"), "2") # Disconnected
***********************************************************

Thanks!

-- 
Lorenzo Mancini
Develer S.r.l.
http://www.develer.com/


More information about the PyQt mailing list