reddish
reddish

Reputation: 1410

Strange behavior using PyQt4's 'pyqtSlot' decorator before another decorator

I have a strange problem with using the QtCore.pyqtSlot decorator in front of a function that is decorated with a different decorator. The problem below demonstrates this problem. It instantiates a minimalistic Qt program with a single button. On pressing the button, the following happens:

(1) signals 'signal_A' and 'signal_B' are connected to the instances method_A and method_B, respectively.

(2) the signal_A and signal_B signals are emitted.

(3) The expected behavior is that method_A and method_B are executed. However, it turns out that both signal_A and signal_B trigger method_A, or method_B (oddly, this nondeterministic, and is different for each run of the program!).

This appears to be a bug.

The issue is present when using the PyQt4 bindings to Qt. It is not present when using PySide (use the '--pyside' parameter to verify). I have seen the problem both in Python 2 and 3, and both in Windows and Linux.

The issue appears to be related to the insertion of a decorator between the '@QtSlot' and the method definition. The 'null_decorator' in the test program should do nothing (if my understanding of decorators is correct), but the behavior of the program changes when I remove it.

Any help in understanding what is going on here would be appreciated.

#! /usr/bin/env python3

import sys, time

if "--pyside" not in sys.argv:
    # default to PyQt4 bindings
    from PyQt4 import QtCore, QtGui
    QtSignal = QtCore.pyqtSignal
    QtSlot   = QtCore.pyqtSlot
else:
    # alternatively, use PySide bindings
    from PySide import QtCore, QtGui
    QtSignal = QtCore.Signal
    QtSlot   = QtCore.Slot


def null_decorator(f):
    def null_decorator_wrapper(self, *args, **kwargs):
        return f(self, *args, **kwargs)
    return null_decorator_wrapper


class TestClass(QtCore.QObject):

    def __init__(self, *args, **kwargs):
        super(TestClass, self).__init__(*args, **kwargs)

    @QtSlot()
    @null_decorator
    def method_A(self):
        print("method_A() executing!")

    @QtSlot()
    @null_decorator
    def method_B(self):
        print("method_B() executing!")


class DemonstrateProblemButton(QtGui.QPushButton):

    signal_A = QtSignal()
    signal_B = QtSignal()

    def __init__(self, *args, **kwargs):
        super(DemonstrateProblemButton, self).__init__(*args, **kwargs)
        self.clicked.connect(self.on_clicked)

    @QtSlot()
    def on_clicked(self):

        # Create TestClass instance
        instance = TestClass()

        # connect the signals
        self.signal_A.connect(instance.method_A)
        self.signal_B.connect(instance.method_B)

        # emit the signals
        self.signal_A.emit()
        self.signal_B.emit()

def main():

    # instantiate the GUI application
    app = QtGui.QApplication(sys.argv)

    button = DemonstrateProblemButton("Demonstrate Problem")
    button.show()

    return QtGui.QApplication.exec_()


if __name__ == "__main__":
    exitcode = main()
    sys.exit(exitcode)

Upvotes: 2

Views: 308

Answers (1)

ekhumoro
ekhumoro

Reputation: 120688

Using the null_decorator like that will result in all the slots having the same name (i.e. "null_decorator_wrapper"), and so PyQt may not be able to distinguish between them.

There are several ways to fix this in your example.

Firstly, you could ensure the slots have different signatures:

@QtSlot()
@null_decorator
def method_A(self):
    print("method_A() executing!")

@QtSlot(int)
@null_decorator
def method_B(self, foo=0):
    print("method_B() executing!")

Secondly, you could explicitly specify the slot names:

@QtSlot(name="method_A")
@null_decorator
def method_A(self):
    print("method_A() executing!")

@QtSlot(name="method_B")
@null_decorator
def method_B(self, foo=0):
    print("method_B() executing!")

Thirdly, you could automagically set the name in the null_decorator:

def null_decorator(f):
    def null_decorator_wrapper(self, *args, **kwargs):
        return f(self, *args, **kwargs)
    null_decorator_wrapper.__name__ = f.__name__
    return null_decorator_wrapper

PS: The behaviour of the pyqtSlot decorator is clearly stated in the PyQt docs (in particular, see the description of the name parameter), and is certainly not a bug.

Upvotes: 2

Related Questions