Search code examples
pythonpyqtpyqt5qt-signalsqobject

Create pyqtSignals from dictionary


Well I was scripting a software designed via Python which I'll be using signals and slots too much in an PyQt5 application. I got an idea of creating a dictionary where all signals come in and each signal will have its own key in order to access (or basically to connect it to a function). The problem is that I get this error 'AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect' for some reason. I read about this error and found out that I have to declare the signals outside the constructor to get it working but unfortunately that will break my idea so that, I came here so somebody can solve my issue.

Here is the code if you still don't understand:

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtCore import QRunnable, pyqtSlot, QThreadPool, QObject, pyqtSignal

class WorkerSignals(QObject):
    signals = {}

    def __init__(self, **kwargs):
        QObject.__init__(self)
        if (kwargs is not None):
            for key, value in kwargs.items():
                self.signals[key] = value

class Worker(QRunnable):
    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        self.fn = fn
        self.args = args
        self.kwargs = kwargs

    @pyqtSlot()
    def run(self):
        self.fn(*self.args, **self.kwargs)

And an example of creating signals:

worker_signals = WorkerSignals(result=pyqtSignal(str), error=pyqtSignal(str))
worker_signals.signals['result'].connect(self.on_receive_result)
worker_signals.signals['error'].connect(self.on_receive_error)

Solution

  • As indicated in the docs:

    A signal (specifically an unbound signal) is a class attribute. When a signal is referenced as an attribute of an instance of the class then PyQt5 automatically binds the instance to the signal in order to create a bound signal. [...]

    So it is not only necessary that is it declared outside the constructor but it must be a static attribute since it serves as a prototype to create the signals that belong to the instance. A possible solution is to use type to create dynamic classes:

    from PyQt5 import QtCore
    
    d = {
        "result": QtCore.pyqtSignal(str),
        "error": QtCore.pyqtSignal(str)
    }
    
    WorkerSignals = type("WorkerSignals", (QtCore.QObject,), d)
    
    if __name__ == '__main__':
        import sys
        app = QtCore.QCoreApplication(sys.argv)
        worker_signals = WorkerSignals()
    
        def on_result(text):
            print("result:", text)
    
        def on_error(text):
            print("error:", text)
    
        worker_signals.result.connect(on_result)
        worker_signals.error.connect(on_error)
    
        def emit_result():
            worker_signals.result.emit(" 1+1=2 ")
        def emit_error():
            worker_signals.error.emit(" :( ")
    
        QtCore.QTimer.singleShot(1000, emit_result)
        QtCore.QTimer.singleShot(2000, emit_error)
    
        QtCore.QTimer.singleShot(3000, app.quit)
        sys.exit(app.exec_())