Schollii Schollii - 3 months ago 22
Python Question

Why crash when derive from QListWidgetItem AND QObject

The following minimal example crashes in pyqt 5.7.1 on windows (copy-paste this in a .py file and run):

from PyQt5.QtWidgets import QListWidgetItem, QListWidget, QApplication
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal


class MyListItem(QListWidgetItem):
def __init__(self, obj):
QListWidgetItem.__init__(self, 'example')
obj.sig_name_changed.connect(self.__on_list_item_name_changed)

def __on_list_item_name_changed(self, new_name: str):
self.setText(new_name)


class MyListItem2(QListWidgetItem, QObject):
def __init__(self, obj):
QListWidgetItem.__init__(self, 'example')
QObject.__init__(self)
obj.sig_name_changed.connect(self.pyqt_slot)

@pyqtSlot(str)
def __on_list_item_name_changed(self, new_name: str):
self.setText(new_name)


class Data(QObject):
sig_name_changed = pyqtSignal(str)


class SearchPanel(QListWidget):
def __init__(self, parent=None):
QListWidget.__init__(self, parent)
obj = Data()
hit_item = MyListItem(obj) # OK
hit_item = MyListItem2(obj) # crashes
self.addItem(hit_item)
obj.sig_name_changed.emit('new_example')


app = QApplication([])
search = SearchPanel()
search.show()
app.exec()


Now just comment out the line that says "crashes", and it works fine. Moreover, the list widget shows 'new_example', showing that the signal went through.

Is there a way to make it work with MyListItem2? i.e. I want to be able to decorate the slot with pyqtSlot, which in turn requires (in PyQt 5.7) that I derive item from QObject.

The intent here is that each item in the list has several characteristics that can change (icon, font, text color) based on signals from associated Data instance (each instance actually "lives", in the Qt sense of the term, in a second thread of our application).

Answer

This has got nothing to do with pyqtSlot.

The actual problem is that you are trying to inherit from two Qt classes, and that is not generally supported. The only exceptions to this are Qt classes that implement interfaces, and Qt classes that share a common base-class (e.g. QListWidget and QWidget). However, only the former is offically supported, and there are several provisos regarding the latter (none of which are relevant here).

So a Python class that inherits from both QListWidgetItem and QObject just will not work. The main problem occurs when PyQt tries to access attributes that are not defined by the top-level base-class (even when the attribute does not exist). In earlier PyQt versions, this would simply raise an error:

>>> class MyListItem2(QListWidgetItem, QObject): pass
...
>>> x = MyListItem2()
>>> x.objectName()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: could not convert 'MyListItem2' to 'QObject'
>>> x.foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: could not convert 'MyListItem2' to 'QObject'

which makes it clear that (in C++ terms) a MyListItem2(QListWidgetItem) cannot be cast to a QObject. Unfortunately, it seems that more recent versions of PyQt5 no longer raise this error, and instead just immediately dump core (which presumably is a bug).

If you really need to use pyqtSlot, one suggestion would be to use composition rather than subclassing. So perhaps something like this:

class ListItemProxy(QObject):
    def __init__(self, item, obj):
        QObject.__init__(self)
        self._item = item
        obj.sig_name_changed.connect(self.__on_list_item_name_changed)

    @pyqtSlot(str)
    def __on_list_item_name_changed(self, new_name: str):
        self._item.setText(new_name)

class MyListItem2(QListWidgetItem):
    def __init__(self, obj):
        QListWidgetItem.__init__(self, 'example')
        self._proxy = ListItemProxy(self, obj)