2

I wanted to use a decorator to handle exceptions in my PyQt5 application:

def handle_exceptions(func):
    def func_wrapper(*args, **kwargs):
        try:
            print(args)
            return func(*args, **kwargs)
        except Exception as e:
            print(e)
            return None
    return func_wrapper


class MainWindow(QMainWindow):

    def __init__(self):
        QMainWindow.__init__(self)
        loadUi("main_window.ui",self)
        self.connect_signals() 

    def connect_signals(self):
        self.menu_action.triggered.connect(self.fun)

    @handle_exceptions
    def fun(self):
        print("hello there!")

When I run I get the following exception:

fun() takes 1 positional argument but 2 were given

The output is False (printed args in the decorator).

The interesting thing is that when I run the fun() function directly by self.fun() in the constructor or comment the decorator, everything works. Seems like the decorator adds an additional argument, but only when the function is called by the signal. What is going on?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
KarolBorkowski
  • 145
  • 1
  • 9

4 Answers4

3

The decorator isn't the problem; you simply haven't defined fun with the correct number of parameters.

@handle_exceptions
def fun(self, foo):
    print("hello there!")

fun is an ordinary function; self.fun is a bound method that, when called, calls fun with self as the first argument and passing its own arguments as additional arguments to fun. Whatever is calling self.fun is passing an additional argument, so the definition of fun has to be accept that.

chepner
  • 497,756
  • 71
  • 530
  • 681
3

The issue is that QAction.triggered emits a boolean when emitted. When a slot receives a signal, the arguments of the signal are submitted in the signature of the slot. When the slot has a shorter signature than the signal the extra arguments are ignored. In your case, the non-decorated function has no input parameters besides self, so the checked argument of QAction.triggered is ignored when the non-decorated function receives the signal. However, the decorated function receives an arbitrary number of arguments, so when the decorated function receives the triggered signal, the checked argument is not ignored which is the extra argument that Python is complaining about.

Heike
  • 24,102
  • 2
  • 31
  • 45
2

The problem is caused because the triggered signal is overload, that is to say it has 2 signatures:

void QAction::triggered(bool checked = false)
QAction.triggered()
QAction.triggered(bool checked)

So by default it sends a boolean(false) that clearly does not accept the "fun" method causing the error.

In this case the solution is to use the @pyqtSlot() decorator to indicate the signature that you must accept:

@pyqtSlot()
@handle_exceptions
def fun(self):
    print("hello there!")
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Either this, or just extend `fun()` to accept this argument. – glglgl Jan 29 '20 at 16:23
  • 1
    @glglgl mmm, sometimes in Qt a signal is overloaded by several types of data, for example the currentIndexChanged signal of the QComboBox that can carry a string or an int so it could bring problems so I always recommend being explicit with the signatures. – eyllanesc Jan 29 '20 at 16:26
0

It's not the decorator which adds this argument, it is the fact that you are dealing with a method.

It would act the same way if you omitted the @handle_exceptions.

What happens?

  • You take self.fun and pass it to self.menu_action.triggered.connect().
  • Whenever a menu action triggers, it (presumably) tries to call the given callable with one argument (maybe an event argument?)
  • But: When you take this self.fun, you don't get the function object itself, but you get what MainWindow.fun.__get__(self) (or alike, I don't remember the exact syntax) gives you, a so-called "bound method object". It is a wrapper object which can be called and deflects the calls to the original function object, but prepends the given self to the argument list.
    • This leads to the fact that this object being called with one argument (the event object?) results in the original function being called with two arguments (self and the event object). As it is not ready to take this additional event object, you get the said error.
glglgl
  • 89,107
  • 13
  • 149
  • 217
  • I edited the question: if I comment the decorator, everything works. Why is that? – KarolBorkowski Jan 29 '20 at 16:18
  • @KarolBorkowski It seems [Heike's answer](https://stackoverflow.com/a/59971341/296974) brings it to the point: Obviously, the QT framework checks the function signatures passed. If they don't indicate you can pass the additional argument, it is not passed. In your case, the function indicates that you can pass it, but then it won't fit at a deeper level. – glglgl Jan 29 '20 at 16:21