2

(sample problematic code and its output at the bottom)

Using PyQt5, I'm writing a QDialog with QTabWidget in it. This tab widget has a QFormLayout layout. I want to iterate over the form and store its right-side widgets (the "fields") as keys in a weakref.WeakKeyDictionary for later use (until the dialog is closed and the respective keys hopefully vanish automatically).

I found out that my weak-keys dict don't work as expected: some widgets are correctly stored, some are not. In particular, widgets added later seem to be stored more often (when I quit and reopen the app many times).

I called print(hex(id(label)) for each label widget label in the form. This showed that some labels have the same Python id, and I believe that only the last iterated over widget with any particular id is being stored.

Is this "id sharing" really the reason why my weak-keys dict doesn't store all widgets? Why doesn't each widget have its own id? Can I change my code such that each widget has a unique id? Can I change my code such that each widget can be stored exactly once in my weak-keys dict?

Sample code:

#!/usr/bin/env python3

from weakref import WeakKeyDictionary

from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.weak = WeakKeyDictionary()
        self.clickme = QPushButton("Click me", self)
        self.clickme.clicked.connect(self.open_settings)

    @pyqtSlot()
    def open_settings(self) -> None:
        dialog = QDialog(self)
        self.weak[dialog] = "Settings"
        grid = QGridLayout()
        tabs = QTabWidget(dialog)
        tab0 = QWidget(tabs)
        tabs.addTab(tab0, "Sample")
        form0 = QFormLayout()
        for char in "sample":
            form0.addRow(char, QLineEdit(tab0))
        tab0.setLayout(form0)
        # print information
        for row in range(form0.rowCount()):
            label = form0.itemAt(row, form0.LabelRole).widget()
            print(hex(id(label)), type(label).__name__)
            self.weak[label] = "foobar"
        print(flush=True)
        for k, v in self.weak.items():
            print(f"{k!r}: {v!r}")
        print(flush=True)
        grid.addWidget(tabs, 0, 0, 1, 3)
        dialog.show()
        dialog.exec()


if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

Output of this sample code when the whole app is running:

0x7f5956285670 QLabel  # this is ok
0x7f5956285700 QLabel  # this is ok
0x7f59562855e0 QLabel  # this is ok
0x7f5956285670 QLabel  # why the repeated id?!
0x7f5956285700 QLabel  # why the repeated id?!
0x7f59562855e0 QLabel  # why the repeated id?!

# the resulting weak-keys dict:
<PyQt5.QtWidgets.QDialog object at 0x7f5956342f70>: 'Settings'
<PyQt5.QtWidgets.QLabel object at 0x7f59562855e0>: 'foobar'
Daniel Diniz
  • 175
  • 8

1 Answers1

2

It seems that it is a bug and it seems to happen when the objects are created in C++ (like the QWidgetItem of the QFormLayout) and then when it is accessed from python, seems that pyqt5 reuses the objects (in pyside2 it does not happen).

A possible solution is to create the QLabels in python so that the objects are not reused.

form0.addRow(QLabel(char), QLineEdit())

Besides that you have to access the QLabel instead of the QWidgetItem:

# print information
for row in range(form0.rowCount()):
    label = form0.itemAt(row, form0.LabelRole).widget()
    print(hex(id(label)), type(label).__name__)
    self.weak[label] = "foobar"
print(flush=True)

Output:

0x7f756703c0d0 QLabel
0x7f756703c1f0 QLabel
0x7f756703c310 QLabel
0x7f756703c430 QLabel
0x7f756703c550 QLabel
0x7f756703c670 QLabel

<PyQt5.QtWidgets.QDialog object at 0x7f756ef61dc0>: 'Settings'
<PyQt5.QtWidgets.QLabel object at 0x7f756703c0d0>: 'foobar'
<PyQt5.QtWidgets.QLabel object at 0x7f756703c1f0>: 'foobar'
<PyQt5.QtWidgets.QLabel object at 0x7f756703c310>: 'foobar'
<PyQt5.QtWidgets.QLabel object at 0x7f756703c430>: 'foobar'
<PyQt5.QtWidgets.QLabel object at 0x7f756703c550>: 'foobar'
<PyQt5.QtWidgets.QLabel object at 0x7f756703c670>: 'foobar'
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • 1
    I'm not completely sure it's actually a bug, considering that PyQt objects are just references to the C++ bindings: as long as the python references exist in the same scope, the ids can be matched. I noticed a similar behavior when dealing with QMetaObjects: while `widget.metaObject() == widget.metaObject()` is True, if you do `x = id(widget.metaObject())` *and then* `y = id(widget.metaObject())` they are not the same. – musicamante Apr 09 '21 at 03:08
  • I forget to add `.widget()` when I transformed my actual code into a minimal example. I edited the question to add it, because my real problem wasn't retrieving the QLabel's. But let me know if I should keep the question as it originally was. – Daniel Diniz Apr 09 '21 at 03:09
  • 1
    @musicamante I say that it is a bug not in the sense that this implementation is incorrect but that it is not documented. – eyllanesc Apr 09 '21 at 03:20