I'm having problems getting a derived class to receive signals properly in PySide. I'm using a transmitter and a receiver on two separate threads from the main (GUI or command-line application) thread. The threads are QThread objects. The transmitter and receiver are moved immediately after creation to their thread using QObject.moveToThread(). If the receiver is derived directly from QObject, all works fine, and the receiver receives within its thread. However, if the receiver is derived from a base class that is derived from QObject, the receiver still receives the signal, but does so on the wrong thread (the main thread).
Example (with some signal debugging code adapted from PyQt & unittest - Testing signal and slots):
#!/usr/bin/env python3
# weigh/bugtest_qt_signal_derived.py
import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
import sys
import threading
import time
from PySide import QtCore
from PySide.QtCore import (
QCoreApplication,
QObject,
QThread,
Signal,
Slot,
)
_oldEmit = QtCore.QObject.emit # normal method
def debug_emit(self, *args):
logger.debug("EMIT: thread name={}, emit args={}".format(
threading.current_thread().name,
repr(args),
))
_oldEmit(self, *args)
QtCore.QObject.emit = debug_emit
def report(msg):
logger.info("{} [{}]".format(msg, threading.current_thread().name))
class Transmitter(QObject):
transmit = Signal()
finished = Signal()
def start(self):
count = 3
logger.info("Starting transmitter")
while count > 0:
time.sleep(1) # seconds
report("transmitting, count={}".format(count))
self.transmit.emit()
count -= 1
logger.info("Stopping transmitter")
self.finished.emit()
class Base(QObject):
def __init__(self, parent=None):
super().__init__(parent=parent)
@Slot()
def start(self):
report("Starting receiver")
@Slot()
def receive(self):
report("receive: BASE")
class Derived(Base):
def __init__(self, parent=None):
super().__init__(parent=parent)
@Slot()
def receive(self):
report("receive: DERIVED")
USE_DERIVED = True
if __name__ == '__main__':
logging.basicConfig()
logger.setLevel(logging.DEBUG)
# Objects
app = QCoreApplication(sys.argv)
tx_thread = QThread()
transmitter = Transmitter()
transmitter.moveToThread(tx_thread)
rx_thread = QThread()
if USE_DERIVED:
receiver = Derived()
else:
receiver = Base()
receiver.moveToThread(rx_thread)
# Signals: startup
tx_thread.started.connect(transmitter.start)
rx_thread.started.connect(receiver.start)
# ... shutdown
transmitter.finished.connect(tx_thread.quit)
tx_thread.finished.connect(rx_thread.quit)
rx_thread.finished.connect(app.quit)
# ... action
transmitter.transmit.connect(receiver.receive)
# Go
rx_thread.start()
tx_thread.start()
report("Starting app")
app.exec_()
Output with USE_DERIVED = False
:
INFO:__main__:Starting app [MainThread]
INFO:__main__:Starting receiver [Dummy-1]
INFO:__main__:Starting transmitter
INFO:__main__:transmitting, count=3 [Dummy-2]
DEBUG:__main__:EMIT: thread name=Dummy-2, emit args=('2transmit()',)
INFO:__main__:receive: BASE [Dummy-1]
INFO:__main__:transmitting, count=2 [Dummy-2]
DEBUG:__main__:EMIT: thread name=Dummy-2, emit args=('2transmit()',)
INFO:__main__:receive: BASE [Dummy-1]
INFO:__main__:transmitting, count=1 [Dummy-2]
DEBUG:__main__:EMIT: thread name=Dummy-2, emit args=('2transmit()',)
INFO:__main__:Stopping transmitter
DEBUG:__main__:EMIT: thread name=Dummy-2, emit args=('2finished()',)
INFO:__main__:receive: BASE [Dummy-1]
Output with USE_DERIVED = True
:
INFO:__main__:Starting app [MainThread]
INFO:__main__:Starting receiver [MainThread]
INFO:__main__:Starting transmitter
INFO:__main__:transmitting, count=3 [Dummy-1]
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2transmit()',)
INFO:__main__:receive: DERIVED [MainThread]
INFO:__main__:transmitting, count=2 [Dummy-1]
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2transmit()',)
INFO:__main__:receive: DERIVED [MainThread]
INFO:__main__:transmitting, count=1 [Dummy-1]
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2transmit()',)
INFO:__main__:Stopping transmitter
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2finished()',)
INFO:__main__:receive: DERIVED [MainThread]
... the difference being that the Base class receives on its own thread, and the Derived class receives on the MainThread.
Does anyone know why? Many thanks!
Software: PySide version: 1.2.4; QtCore version: 4.8.6; Ubuntu 14.04; Python 3.4.4.
Further to @101's comment:
The signal override isn't necessary for failure. These derived classes also fail (in the sense of being called in the wrong thread):
class DerivedTwo(Base):
def __init__(self, parent=None):
super().__init__(parent=parent)
class DerivedThree(Base):
def __init__(self, parent=None):
QObject.__init__(self, parent=parent)
Since the output suggests the derived receiver object is starting on the wrong thread, I wondered if the problem is that QObject.moveToThread()
fails for derived objects. However, that doesn't seem to be the case:
def debug_object(obj):
logger.debug("Object {} belongs to QThread {}".format(obj, obj.thread()))
def debug_thread(thread_name, thread):
logger.debug("{} is QThread {}".format(thread_name, thread))
# ...
tx_thread = QThread()
debug_thread("tx_thread", tx_thread)
transmitter = Transmitter()
debug_object(transmitter)
transmitter.moveToThread(tx_thread)
debug_object(transmitter)
rx_thread = QThread()
debug_thread("rx_thread", rx_thread)
receiver = DerivedTwo()
debug_object(receiver)
receiver.moveToThread(rx_thread)
debug_object(receiver)
gives
DEBUG:__main__:tx_thread is QThread <PySide.QtCore.QThread object at 0x7fc4a3befd08>
DEBUG:__main__:Object <__main__.Transmitter object at 0x7fc4a3bf2648> belongs to QThread <PySide.QtCore.QThread object at 0x7fc4a3bf2688>
DEBUG:__main__:Object <__main__.Transmitter object at 0x7fc4a3bf2648> belongs to QThread <PySide.QtCore.QThread object at 0x7fc4a3befd08>
DEBUG:__main__:rx_thread is QThread <PySide.QtCore.QThread object at 0x7fc4a3bf2708>
DEBUG:__main__:Object <__main__.DerivedTwo object at 0x7fc4a3bf2788> belongs to QThread <PySide.QtCore.QThread object at 0x7fc4a3bf2688>
DEBUG:__main__:Object <__main__.DerivedTwo object at 0x7fc4a3bf2788> belongs to QThread <PySide.QtCore.QThread object at 0x7fc4a3bf2708>
INFO:__main__:Starting app [MainThread]
INFO:__main__:Starting receiver [MainThread]
INFO:__main__:Starting transmitter [Dummy-1]
INFO:__main__:transmitting, count=3 [Dummy-1]
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2transmit()',)
INFO:__main__:receive: BASE [MainThread]
...
which suggests to me that the derived object gets transferred correctly to a new thread (notionally, for Qt events processing) during moveToThread()
, but then is started on (and receives on) the main thread somehow.
Additional: it works in C++ Qt
Header:
// bugtest_qt_signal_derived.h
#include <QtCore/QCoreApplication>
#include <QtCore/QtDebug> // not QDebug
#include <QtCore/QObject>
#include <QtCore/QString> // works with qDebug where std::string doesn't
#include <QtCore/QThread>
void debug_object(const QString& obj_name, const QObject& obj);
void debug_thread(const QString& thread_name, const QThread& thread);
void report(const QString& msg);
class Transmitter : public QObject
{
Q_OBJECT // enables macros like "signals:", "slots:", "emit"
public:
Transmitter() {}
virtual ~Transmitter() {}
signals:
void transmit();
void finished();
public slots:
void start();
};
class Base : public QObject
{
Q_OBJECT
public:
Base() {}
public slots:
void start();
void receive();
};
class Derived : public Base
{
Q_OBJECT
public:
Derived() {}
public slots:
void receive();
};
Source:
// bugtest_qt_signal_derived.cpp
#include "bugtest_qt_signal_derived.h"
#include <unistd.h> // for sleep()
void debug_object(const QString& obj_name, const QObject& obj)
{
qDebug() << "Object" << obj_name << "belongs to QThread" << obj.thread();
}
void debug_thread(const QString& thread_name, const QThread& thread)
{
qDebug() << thread_name << "is QThread at" << &thread;
}
void report(const QString& msg)
{
qDebug().nospace() << msg << " [" << QThread::currentThreadId() << "]";
}
void Transmitter::start()
{
unsigned int count = 3;
report("Starting transmitter");
while (count > 0) {
sleep(1); // seconds
report(QString("transmitting, count=%1").arg(count));
emit transmit();
count -= 1;
}
report("Stopping transmitter");
emit finished();
}
void Base::start()
{
report("Starting receiver");
}
void Base::receive()
{
report("receive: BASE");
}
void Derived::receive()
{
report("receive: DERIVED");
}
#define USE_DERIVED
int main(int argc, char* argv[])
{
// Objects
QCoreApplication app(argc, argv);
QThread tx_thread;
debug_thread("tx_thread", tx_thread);
Transmitter transmitter;
debug_object("transmitter", transmitter);
transmitter.moveToThread(&tx_thread);
debug_object("transmitter", transmitter);
QThread rx_thread;
debug_thread("rx_thread", rx_thread);
#ifdef USE_DERIVED
Derived receiver;
#else
Base receiver;
#endif
debug_object("receiver", receiver);
receiver.moveToThread(&rx_thread);
debug_object("receiver", receiver);
// Signals: startup
QObject::connect(&tx_thread, SIGNAL(started()),
&transmitter, SLOT(start()));
QObject::connect(&rx_thread, SIGNAL(started()),
&receiver, SLOT(start()));
// ... shutdown
QObject::connect(&transmitter, SIGNAL(finished()),
&tx_thread, SLOT(quit()));
QObject::connect(&tx_thread, SIGNAL(finished()),
&rx_thread, SLOT(quit()));
QObject::connect(&rx_thread, SIGNAL(finished()),
&app, SLOT(quit()));
// ... action
QObject::connect(&transmitter, SIGNAL(transmit()),
&receiver, SLOT(receive()));
// Go
rx_thread.start();
tx_thread.start();
report("Starting app");
return app.exec();
}
Output:
"tx_thread" is QThread at QThread(0x7ffc138c5330)
Object "transmitter" belongs to QThread QThread(0xdae1e0)
Object "transmitter" belongs to QThread QThread(0x7ffc138c5330)
"rx_thread" is QThread at QThread(0x7ffc138c5350)
Object "receiver" belongs to QThread QThread(0xdae1e0)
Object "receiver" belongs to QThread QThread(0x7ffc138c5350)
"Starting app" [0x7f032fb32780]
"Starting transmitter" [0x7f032ae77700]
"Starting receiver" [0x7f032b678700]
"transmitting, count=3" [0x7f032ae77700]
"receive: DERIVED" [0x7f032b678700]
"transmitting, count=2" [0x7f032ae77700]
"receive: DERIVED" [0x7f032b678700]
"transmitting, count=1" [0x7f032ae77700]
"Stopping transmitter" [0x7f032ae77700]
"receive: DERIVED" [0x7f032b678700]
Additional: It also works in PyQt
Code:
#!/usr/bin/env python2
import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
import sys
import threading
import time
from PyQt4.QtCore import (
QCoreApplication,
QObject,
QThread,
pyqtSignal,
pyqtSlot,
)
def debug_object(obj):
logger.debug("Object {} belongs to QThread {}".format(obj, obj.thread()))
def debug_thread(thread_name, thread):
logger.debug("{} is QThread {}".format(thread_name, thread))
def report(msg):
logger.info("{} [{}]".format(msg, threading.current_thread().name))
class Transmitter(QObject):
transmit = pyqtSignal()
finished = pyqtSignal()
def start(self):
count = 3
report("Starting transmitter")
while count > 0:
time.sleep(1) # seconds
report("transmitting, count={}".format(count))
self.transmit.emit()
count -= 1
report("Stopping transmitter")
self.finished.emit()
class Base(QObject):
def __init__(self, parent=None):
super(Base, self).__init__(parent=parent)
@pyqtSlot()
def start(self):
report("Starting receiver")
@pyqtSlot()
def receive(self):
report("receive: BASE")
class Derived(Base):
def __init__(self, parent=None):
super(Derived, self).__init__(parent=parent)
@pyqtSlot()
def receive(self):
report("receive: DERIVED")
USE_DERIVED = True
if __name__ == '__main__':
logging.basicConfig()
logger.setLevel(logging.DEBUG)
# Objects
app = QCoreApplication(sys.argv)
tx_thread = QThread()
debug_thread("tx_thread", tx_thread)
transmitter = Transmitter()
debug_object(transmitter)
transmitter.moveToThread(tx_thread)
debug_object(transmitter)
rx_thread = QThread()
debug_thread("rx_thread", rx_thread)
if USE_DERIVED:
receiver = Derived()
else:
receiver = Base()
debug_object(receiver)
receiver.moveToThread(rx_thread)
debug_object(receiver)
# Signals: startup
tx_thread.started.connect(transmitter.start)
rx_thread.started.connect(receiver.start)
# ... shutdown
transmitter.finished.connect(tx_thread.quit)
tx_thread.finished.connect(rx_thread.quit)
rx_thread.finished.connect(app.quit)
# ... action
transmitter.transmit.connect(receiver.receive)
# Go
rx_thread.start()
tx_thread.start()
report("Starting app")
app.exec_()
Output:
DEBUG:__main__:tx_thread is QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad0770>
DEBUG:__main__:Object <__main__.Transmitter object at 0x7fd0b7ad0808> belongs to QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad08a0>
DEBUG:__main__:Object <__main__.Transmitter object at 0x7fd0b7ad0808> belongs to QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad0770>
DEBUG:__main__:rx_thread is QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad08a0>
DEBUG:__main__:Object <__main__.Derived object at 0x7fd0b7ad0938> belongs to QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad09d0>
DEBUG:__main__:Object <__main__.Derived object at 0x7fd0b7ad0938> belongs to QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad08a0>
INFO:__main__:Starting app [MainThread]
INFO:__main__:Starting transmitter [Dummy-1]
INFO:__main__:Starting receiver [Dummy-2]
INFO:__main__:transmitting, count=3 [Dummy-1]
INFO:__main__:receive: DERIVED [Dummy-2]
INFO:__main__:transmitting, count=2 [Dummy-1]
INFO:__main__:receive: DERIVED [Dummy-2]
INFO:__main__:transmitting, count=1 [Dummy-1]
INFO:__main__:Stopping transmitter [Dummy-1]
INFO:__main__:receive: DERIVED [Dummy-2]
Confirm @101's findings in Python 3
Just as described below. All works fine just by removing all @Slot() decorators.
So it seems to be a PySide bug relating to the Slot decorator.
Many thanks!