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

3

8

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.

Digenesis answered 11/5, 2018 at 14:33 Comment(0)
B
6

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.

Braddock answered 11/5, 2018 at 15:56 Comment(0)
L
2

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.

Lavinia answered 14/8, 2020 at 11:52 Comment(4)
Note: I didn't attempt to keep the signals bound to a single instance of a class (i.e. prevent FunkyDynamicSignals().example usage after example has been constructed in another instance. Anything is possible with Python though... :)Lavinia
Again, this is creating classes not signals dynamically. The OP specifically asked whether its possible "to create pyqtSignals at runtime without being class vars".Braddock
At no point do I claim that it is possible to create signals dynamically. What I'm pointing out in my answers is that from a user/API perspective, it is perfectly doable to create signals without manually declaring class variables. The example in this answer shows one such approach - from a user perspective you simply call a method and it will add a signal, at no point does the user know that they are really creating a new class and morphing the instance. I don't much like the code in this answer (it is ugly to change the class in the way that I have), but it does answer the question IMO.Lavinia
Here is the point where you do that: "Contrary to @ekhumoro's accepted answer, I would claim that it is actually possible to add signals at runtime". My answer is specifically about adding signals to instances at runtime, because that is what the OP asked about (see also their code and error message). Your answers relate to a more general topic, and are in no way "contrary" to mine. In fact, I don't see why it is necessary to reference my answer at all in yours.Braddock
L
1

@ekhumoro's accepted answer is, as far as I know, entirely accurate (specifically the statement: "They must be part of the class definition and cannot be dynamically added as class attributes after the class has been defined.").

Unfortunately I've seen this answer mis-interpreted as "It is not possible to generate Qt signals programmatically", which of course, when it comes to Python, is an entirely different proposition.

So I'm afraid this answer does not address the original question, which genuinely wanted to add signals at runtime and instead I thought I'd set the record straight and provide an example of creating signals programmatically, without contradicting the above statement. The solution is to create a dynamically generated class along with some dynamically generated signals. There are several good ways of generating classes dynamically in Python:

Option 1 - using the type function

from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtCore import QMetaObject


DynamicSignal = type("DynamicSignal", (QObject, ), {
    "my_custom_signal": pyqtSignal([str]),
})


if __name__ == '__main__':
    dynamic = DynamicSignal()
    dynamic.my_custom_signal.connect(print)

    dynamic.my_custom_signal.emit("Hello world")

This prints "Hello world" when executed.

Option 2 - using metaclasses

We can also achieve the above with a metaclass:

from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtCore import QMetaObject


class DynamicSignalMeta(type(QObject)):
    def __new__(cls, name, bases, dct):
        dct['my_custom_signal'] = pyqtSignal([str])
        return super().__new__(cls, name, bases, dct)


class DynamicSignal(QObject, metaclass=DynamicSignalMeta):
    pass


if __name__ == '__main__':
    dynamic = DynamicSignal()
    dynamic.my_custom_signal.connect(print)

    dynamic.my_custom_signal.emit("Hello world")
Lavinia answered 14/8, 2020 at 10:51 Comment(3)
This does not create any signals dynamically: it merely creates classes dynamically (which happen to include pyqt signals). The OP is asking how to add pyqt signals at runtime to an instance of a pre-existing class.Braddock
I think my caveat at the start of the answer puts the appropriate context in place. I don't think either of our answers are incorrect, at no point do I contest that it is possible to directly make signals dynamically, but I've seen your answer used to argue that it isn't possible to make any signals dynamically (even indirectly). This latter extension of your precise wording is of course is entirely inaccurate - as you just have to create a class with the appropriate definition. The objective with these answers was to balance out that incorrect interpretation.Lavinia
Well, I'll have to take your word for it about these "incorrect interpretations", since you don't provide any direct citations. Anyway, I've edited the question in order to avoid any further doubt.Braddock

© 2022 - 2024 — McMap. All rights reserved.