Reputation: 1410
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
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