Search code examples
pythonpyqt5signals-slots

PyQt5: Using decorators with methods connected to signals


I want to use a decorator to handle exceptions within a class. However, when using the decorator, it always gives me:

TypeError: f1() takes 1 positional argument but 2 were given

I am quite sure it occurs due to self, because it works perfectly outside the class. But I cannot manage to use it inside the class. Can someone help me?

Here is a MWE:

from PyQt5.QtWidgets import QPushButton, QVBoxLayout, QApplication, QWidget
import sys


def report_exceptions(f):
    def wrapped_f(*args, **kwargs):
        try:
            f(*args, **kwargs)
        except Exception as e:
            print(f"caught: {e}")   
    return wrapped_f


class Window(QWidget):
    def __init__(self):
        super().__init__()
        b1 = QPushButton('1')
        b1.clicked.connect(self.f1)
        layout = QVBoxLayout(self)
        layout.addWidget(b1)
        self.setLayout(layout)

    @report_exceptions
    def f1(self):
        raise Exception("Error inside f1")


app = QApplication([])
window = Window()
window.show()

sys.exit(app.exec_())

Solution

  • The error occurs because the clicked signal sends a default checked parameter which the f1 method does not provide an argument for. There are several ways to work around this:

    • change the signature to allow for the extra argument:

       def f1(self, checked=False):
      
    • use a lambda to consume the unwanted argument:

       b1.clicked.connect(lambda: self.f1())
      
    • wrap the method as a pyqt slot:

       @QtCore.pyqtSlot()
       @report_exceptions
       def f1(self):
      

    The reason why this last one works is because signals with default arguments are treated as two separate overloads: one which which sends the parameter, and one which doesn't. The slot decorator therefore allows you to explicitly select which one is required (the other overload being @QtCore.pyqtSlot(bool)).

    Another peculiarity specific to PyQt is that signals connected to undecorated slots will normally ignore unused parameters. The lambda solution above takes advantage of this mechanism, but your report_exceptions decorator effectively bypasses it, which is why you get the TypeError.