Why do similar signal/slot connections behave differently after Pyqt5 to PySide6 port

Question:

I’m porting a pyqt5 GUI to pyside6 and I’m running into an issue I don’t understand.

I have two QSpinboxes that control two GUI parameters:
One spinbox controls the weight of a splitter,
the other controls the row height in a QtableView.

This is the code in pyqt5:

spinbox1.valueChanged.connect(my_splitter.setHandleWidth)
spinbox2.valueChanged.connect(my_view.verticalHeader().setDefaultSectionSize)

In Pyside6 spinbox1 works fine, but spinbox2 doesn’t do its job and there is this warning:

You can’t add dynamic slots on an object originated from C++.

The issue can be solved by changing the second line of code to:

spinbox2.valueChanged.connect(lambda x: my_view.verticalHeader().setDefaultSectionSize(x))

It’s nice to have found a solution, but I would also like to understand why the two connections behave differently in PySide6 and why using he lambda solves the issue.

The warning message probably holds a clue but I have no idea what dynamic slots are (and a quick google didn’t help me much).

Edit:
Since I was changing two things: Qt5 > QT6, And pyqt > pyside
I looked at this in 4 python wrappers (pyqt5, pyqt6, pyside2, pyside6) to see which of the changes caused the issue.
And I can tell that both pyside 2 and 6 show this behaviour, and none of the pyqt’s

Asked By: mahkitah

||

Answers:

Basically, in Qt the meta object system maintains the communication part. Signals, slots and their communication.

To my understanding, the implementation side of pyside looks for the availability of the slot provided, and if not found the above message "You can’t add dynamic slots on an object originated from C++." is thrown.

Look for that message in below link.

https://github.com/pyside/PySide/blob/master/PySide/QtCore/glue/qobject_connect.cpp

The first slot in your question controlling splitter may be statically available (while compiling) and the pyside implementation is able to find it. So no error.

The lambda version might have worked because it’s anonymous function and pyside implementation might have bypassed searching for it in the list of slots available.

Answered By: Pavan Chandaka

It looks like PySide (or, to be precise, Shiboken, the wrapper that allows Python access to Qt objects) is not able to directly connect to slots of objects directly created by Qt.

That seems a PySide bug: PyQt does not show that behavior, meaning that it’s completely possible to achieve it.

A similar behavior sometimes happens with PyQt as well, but that’s only in very specific cases: for protected methods of objects directly created by Qt. For example, trying to call initStyleOption() of the default delegate of an item view raises a RuntimeError ("no access to protected functions or signals for objects not created from Python").

Still, that should not happen for public functions like setDefaultSectionSize() is.

There are possible workarounds for that, though.

Using a lambda

As you already found out, you can just use a lambda. This will force PySide to connect to a python function instead of a Qt one: PySide always allows that kind of connection.

The drawback of this approach is the common problem with lambdas: if you directly use it as the connect() argument, you completely lose any reference to it, so there is no way to specifically disconnect from that function, unless you disconnect all functions for that signal (or the whole object).

Lambdas can be referenced to, though:

        header = my_view.verticalHeader()
        header.setDefaultSectionSize_ = lambda s: header.setDefaultSectionSize(s)
        spinbox2.valueChanged.connect(header.setDefaultSectionSize_)

With the code above, you can disconnect the function, since you now have a persistent reference to it.

Using a method

This is similar to the above, with the difference that we create a specific method to handle that, assuming that you keep a reference to the header (or, better, the view):

        spinbox2.valueChanged.connect(self.updateMyViewSectionSize)

    def updateMyViewSectionSize(self, size):
        self.my_view.verticalHeader().setDefaultSectionSize(size)

It might be a bit more verbose, but it’s also a better approach, since it consider the dynamic nature of verticalHeader() and provides public access to a function you may need to call in other cases.

Explicitly set the header

This is a trick I normally use for the delegate issue mentioned above: whenever I need to get some info from the delegate based on its initStyleOption(), I just create a new "dummy" delegate; since it has been created in Python, the problem doesn’t occur anymore.

The same works for this case too: create and set a dummy header view.

        my_view.setVerticalHeader(QHeaderView(Qt.Vertical, my_view))

Note that both the orientation and arguments are mandatory, and that the above should always be done as soon as possible (right after the table widget has been created).


I would still suggest you to file a report in the Qt bug tracker, hoping they will be able to fix it at least for PySide6 (PySide2 will probably be ignored, but you never know).

Answered By: musicamante
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.