PySide-6.6: clicked signal sends extra boolean argument to slot

300 views Asked by At

I just upgraded PySide6 (PySide6==6.6.0 from 6.2.2) and a new behavior is wreaking havoc with my GUI program. Every place where I have a clicked signal, the hooked up slot is receiving an extra bool==False argument.

x = QPushButton('New', clicked = self.new_append_title)
# ..... add x to my gui

def new_append_title(self, *args, **kwargs): 
    print(f' Args: {args}' )
    print(f' Kwargs: {kwargs}' )

This returns
Args: (False,) Kwargs: {}

Previously with earlier versions I was getting no arguments. I can fix all my code so it takes an extra dummy argument, but does anybody know what is going on here? I also updated a few other things in my requirements - like moving to python 3.12, so it might not be a PySide issue...


In response to ekhumoro I am actually trying to send and argument here. I have tried every permutation I can think of, including the one he gives in his answer below.

self.save_button.clicked.connect(lambda *, row=row: self.save_row(row))

def save_row(self, row):
    print(f'row={row})

Finally worked. I am not clear on what * does, but it seems to hold the arg from qPushButton signal and therefore it is not necessary to have it in the member function.

1

There are 1 answers

8
ekhumoro On BEST ANSWER

I can indeed reproduce this odd difference in behaviour between various versions of PySide. However, unlike my previous answer on this topic, the current issue relates to how the signal connections are made. It seems PySide has been highly inconsistent in this regard, whereas PyQt is much more reliable (at least in the few versions I tested with).

The important difference with the current case, is that signal connections made via the clicked keyword argument, do not behave the same way as explicit connections made via the connect method. The whole sorry mess is detailed below, but the immediate take-home message here is this: to future-proof your PySide code, do not ever rely on default signal arguments being silently dropped. Always assume that some future version will start sending the default arguments, and protect your slots accordingly. This is how PyQt5/6 has always behaved, and it seems likely that PySide6 will eventually do likewise. In my own code, I have most commonly employed keyword-only arguments to work-around this issue, like this:

button.connect(self.slot)
...
def slot(self, *, mykeyword=True):

As you will see below, this behaves the same way across all versions of PySide and PyQt I tested, and is possibly the least intrusive in terms of API changes and general code-churn. (But see also my previous answer for several other options - with some provisos).


Now for the gory details.

Below is a test script that exposes the problems. As you will see from the output, both PySide2 and PySide6 show different behaviour depending on how the signal connection is made, and on the precise version used. The good news is that PySide-6.6.0 shows the same behaviour as PyQt5/6 when making connections via the clicked keyword argument. The bad news is that using connect shows the same inconsistent behaviour as PySide2. Note also that PySide2 itself shows some differences in how slot arguments are treated: when using connect(slot), the first argument is over-written, whereas with clicked=slot, it isn't.

TEST SCRIPT:

import sys

if (pkg := sys.argv[1]) == 'ps2':
   from PySide2 import QtCore, QtWidgets
elif pkg == 'ps6':
    from PySide6 import QtCore, QtWidgets
elif pkg == 'pq6':
    from PyQt6 import QtCore, QtWidgets
else:
    from PyQt5 import QtCore, QtWidgets

try:
    KEYWORD = sys.argv[2] == 'kw'
except:
    KEYWORD = False

print(f'{QtCore.__package__} (Qt-{QtCore.qVersion()}), keyword={KEYWORD}')

app = QtWidgets.QApplication(['Test'])

def slot1(n=2): print(f'slot1: {n=!r}')
def slot2(*args, n=2): print(f'slot2: {args=!r}, {n=!r}')
def slot3(x, n=2): print(f'slot3: {x=!r}, {n=!r}')
def slot4(*, n=2): print(f'slot4: {n=!r}')

for index in range(1, 5):
    slot = globals()[f'slot{index}']
    if KEYWORD:
        button = QtWidgets.QPushButton(clicked=slot)
    else:
        button = QtWidgets.QPushButton()
        button.clicked.connect(slot)
    button.click()

OUTPUT:

PySide2:

$ python3 test-signals.py ps2
PySide2 (Qt-5.15.11), keyword=False
slot1: n=False
slot2: args=(), n=2
TypeError: slot3() missing 1 required positional argument: 'x'
slot4: n=2

$ python3 test-signals.py ps2 kw
PySide2 (Qt-5.15.11), keyword=True
slot1: n=2
slot2: args=(), n=2
TypeError: slot3() missing 1 required positional argument: 'x'
slot4: n=2

PySide6:

$ python3 test-signals.py ps6
PySide6 (Qt-6.2.2), keyword=False
slot1: n=False
slot2: args=(), n=2
TypeError: slot3() missing 1 required positional argument: 'x'
slot4: n=2

$ python3 test-signals.py ps6 kw
PySide6 (Qt-6.2.2), keyword=True
slot1: n=2
slot2: args=(), n=2
TypeError: slot3() missing 1 required positional argument: 'x'
slot4: n=2

$ python3 test-signals.py ps6
PySide6 (Qt-6.6.0), keyword=False
slot1: n=False
slot2: args=(), n=2
TypeError: slot3() missing 1 required positional argument: 'x'
slot4: n=2

$ python3 test-signals.py ps6 kw
PySide6 (Qt-6.6.0), keyword=True
slot1: n=False
slot2: args=(False,), n=2
slot3: x=False, n=2
slot4: n=2

PyQt5/6:

$ python3 test-signals.py pq6
PyQt6 (Qt-6.6.0), keyword=False
slot1: n=False
slot2: args=(False,), n=2
slot3: x=False, n=2
slot4: n=2

$ python3 test-signals.py pq6 kw
PyQt6 (Qt-6.6.0), keyword=True
slot1: n=False
slot2: args=(False,), n=2
slot3: x=False, n=2
slot4: n=2

$ python3 test-signals.py pq5
PyQt5 (Qt-5.15.11), keyword=False
slot1: n=False
slot2: args=(False,), n=2
slot3: x=False, n=2
slot4: n=2

$ python3 test-signals.py pq5 kw
PyQt5 (Qt-5.15.11), keyword=True
slot1: n=False
slot2: args=(False,), n=2
slot3: x=False, n=2
slot4: n=2