Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to create pyqtSignals on instances at runtime without using class variables?

Is there any possibility to create signals at runtime when needed?

I'm doing something like this in a function:

class WSBaseConnector(QObject)

    def __init__(self) -> None:
        super(QObject, self).__init__()    
        self._orderBookListeners: Dict[str, pyqtSignal[OrderBookData]] = {}

    def registerOrderBookListener(self, market: str, listener: Callable[[OrderBookData], None], loop: AbstractEventLoop) -> None:
            try:
                signal = self._orderBookListeners[market]
            except KeyError:
                signal = pyqtSignal(OrderBookData)
                signal.connect(listener)
                self._orderBookListeners[market] = signal
            else:
                signal.connect(listener)

As you can see, I have a dict that stores str, pyqtSignal pairs. When I try to connect the signal to the listener I get the error:

'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect'

Is it not possible to create pyqtSignals at runtime without been class vars?

Cheers.

like image 225
Notbad Avatar asked May 11 '18 14:05

Notbad


2 Answers

No, it is not possible. The pyqtSignal object is a factory function that returns a descriptor, so it must be created when the class statement is executed. To quote from the docs:

New signals should only be defined in sub-classes of QObject. They must be part of the class definition and cannot be dynamically added as class attributes after the class has been defined.

New signals defined in this way will be automatically added to the class’s QMetaObject. This means that they will appear in Qt Designer and can be introspected using the QMetaObject API. [emphasis added]

Your code is creating unbound signal objects, which is why you get the attribute error. The distinction between bound and unbound signals is exactly the same as with the methods of classes. To quote again from 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. This is the same mechanism that Python itself uses to create bound methods from class functions.

like image 143
ekhumoro Avatar answered Oct 18 '22 10:10

ekhumoro


In my other answer I focused on the question "Is it possible to programmatically add signals" as opposed to what the OP asked "Is it possible to dynamically add signals at runtime (i.e. after the class has been instantiated)".

Contrary to @ekhumoro's accepted answer, I would claim that it is actually possible to add signals at runtime, despite the PyQT documentation's very clear statement:

They must be part of the class definition and cannot be dynamically added as class attributes after the class has been defined

Whilst I don't doubt the accuracy of the statement, Python is a wonderfully dynamic language and it is in-fact reasonably easy to achieve the desired result. The problem we have to overcome is that in order to add signals at runtime, we must create a new class definition and modify an instance's underlying class. In Python this can be achieved by setting an object's __class__ attribute (which in general has a number of issues to be aware of).

from PyQt5.QtCore import QObject, pyqtSignal


class FunkyDynamicSignals(QObject):
    def add_signal(self, name, *args):
        # Get the class of this instance.
        cls = self.__class__

        # Create a new class which is identical to this one,
        # but which has a new pyqtSignal class attribute called of the given name.
        new_cls = type(
            cls.__name__, cls.__bases__,
            {**cls.__dict__, name: pyqtSignal(*args)},
        )
        # Update this instance's class with the newly created one.
        self.__class__ = new_cls  # noqa

With this class we can create signals after the object has been instantiated:

>>> dynamic = FunkyDynamicSignals()
>>> dynamic.add_signal('example', [str])
>>> dynamic.example.connect(print)

>>> dynamic.add_signal('another_example', [str])
>>> dynamic.another_example.connect(print)

>>> dynamic.example.emit("Hello world")
Hello world

This approach uses modern Python syntax (but could equally have been written for Py2), is careful to expose a sensible class hierarchy and preserves existing connections when new signals are added.

like image 24
pelson Avatar answered Oct 18 '22 10:10

pelson