1

I've a QSqlQueryModel which fetches IDs like 1, 2,, etc. In the UI we display this field as a 4-digit int: 0001, 0002, etc.

Here's my proxy subclass of QIdentityProxyModel to add the zero prefix:

class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    def data(self, index, role):
        d = self.sourceModel().data(index, role)
        return f'{d:04}' if role in (Qt.DisplayRole, Qt.EditRole) else d

# Uncommenting this works
#    def rowCount(self, parent=QModelIndex()):
#        return self.sourceModel().rowCount(parent)

Here's how I set it to a QLineEdits completer:

def setIdCompleterModel(self):
    # model is a loaded QSqlQueryModel
    proxy = ZeroPrefixProxy(self.ui.txtId)
    proxy.setSourceModel(model)
    self.ui.txtId.setCompleter(QCompleter(proxy, self.ui.txtId))
    # Uncommenting this works
    # proxy.data(proxy.index(0, 0), Qt.DisplayRole)

No suggestions are displayed irrespective of what I type (1 or 0001). However, when I uncomment either snippets above things work great.

I do not want to do either as they seem pointless:

  • QIdentityProxyModel already implements columCount (it works correctly)
  • I've no reason to call data (I originally wrote it just to test)

What am I missing? Why is the simple subclass implementation not working?

Setup: ArchLinux, Qt 5.15.10, PySide2 5.15.2.1

MCVE

This code works on my setup only if I comment out ZeroPrefixProxy.data:

import sys

from PySide2.QtCore import Qt, QIdentityProxyModel, QModelIndex
from PySide2.QtWidgets import QApplication, QMainWindow, QLineEdit, QCompleter
from PySide2.QtGui import QStandardItem, QStandardItemModel

class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    # Commenting this method out makes things work
    def data(self, index, role=Qt.DisplayRole):
        return self.sourceModel().data(index, role)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        useStdModel = False
        if useStdModel:
            model = QStandardItemModel(5, 1, self)
            for i in range(1, 6):
                item = QStandardItem()
                # setData as ctor only takes an str, we need an int
                item.setData(i, Qt.EditRole)
                model.setItem(i-1, 0, item)
        else:
            db = QSqlDatabase.addDatabase('QPSQL')
            host = 'localhost'
            dbname = 'test'
            db.setHostName(host)
            db.setDatabaseName(dbname)
            db.setUserName('pysider')
            if not db.open():
                print('DB not open')
            model = QSqlQueryModel()
            model.setQuery('SELECT id FROM items')

        proxy = ZeroPrefixProxy(self)
        proxy.setSourceModel(model)
        lineEdit = QLineEdit()
        comp = QCompleter(proxy, lineEdit)
        lineEdit.setCompleter(comp)
        self.setCentralWidget(lineEdit)

app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
legends2k
  • 31,634
  • 25
  • 118
  • 222
  • I cannot reproduce this. With or without the commented lines, it works fine for me with both PySide2 and PyQt5. This is using Qt-5.15.10 with PyQt-5.15.9 or PySide-5.15.10 on arch-linux. Please therefore provide a proper [mre], and state the exact versions of Qt5 and PySide2 you are using. – ekhumoro Jul 17 '23 at 13:17
  • @ekhumoro Added both MCVE and version info. I've a feeling I'm just sub-classing it wrong. Thanks for looking; please let me know if I've to add further details.. – legends2k Jul 17 '23 at 14:21
  • You're trying to get the value from the index of the proxy, but you're not mapping it to the source. That line should be `self.sourceModel().data(self.mapToSource(index), role)`. – musicamante Jul 17 '23 at 15:26
  • @musicamante That fixed it! Please add it as answer. Though I'm surprised that _identity_ remaps the indices; even [`QIdentityProxyModel` documentation example doesn't use `mapToSource`](https://doc.qt.io/qt-5.15/qidentityproxymodel.html#details). I was under the assumption that the indices will be identical and no remapping is needed. Another episode of wrong assumptions and time sink. – legends2k Jul 17 '23 at 15:55
  • 1
    Mapping indexes shouldn't make much difference with a QSqlQueryModel, since that uses explicit row and column numbers internally. In my test-case, I already tried it with and without mapping indexes and, for me, it works fine either way - which is partly why I asked for a proper MRE. Hoowever, your latest example isn't really equivalent to the original, since it uses a dfiffernt kind of model. – ekhumoro Jul 17 '23 at 16:39
  • 1
    @legends2k The example seems to be wrong for another reason, actually: the call to `sourceModel()->data()` is missing the index argument completely. – musicamante Jul 17 '23 at 17:17
  • @ekhumoro You're right. MCVE with `QSqlQueryModel` works irrespective of mapping. The issue was `QSqlQueryModel()` vs `QSqlQueryModel(self)`; without a parent the model gets destroyed silently. Silly mistake – legends2k Jul 17 '23 at 23:04
  • @ekhumoro However, it's still strange that MCVE with `QStandardItemModel` only works with the mapping. Is this expected? I mean `QIdentityProxyModel` doesn't to anything fancy in `mapToSource()`? – legends2k Jul 17 '23 at 23:06
  • 1
    The reason MVCE with `QStandardItemModel` works only with the mapping is every `QModelIndex` is tied to the model that created it; it has to be remapped. [Old Qt forum post](https://www.qtcentre.org/threads/61054-QStandardItemModel-with-QIdentityProxyModel) explains this. – legends2k Jul 17 '23 at 23:21
  • @ekhumoro In case, I've updated MCVE to contain `QSQLQueryModel` case too. I'd be happy to upvote both your answer (about incomplete MCVE which led me to find the parent issue) and @musicamante's (remapping). I learned a bit from both, thanks. I can close this question but feel this can be useful to folks in the future. – legends2k Jul 17 '23 at 23:50
  • 1
    @legends2k Glad you sorted it out. Perhaps it would be best if you summarised what you discovered in a self-answer? – ekhumoro Jul 18 '23 at 00:03
  • @ekhumoro Added my answer! Perhaps recent Python use has made this C++ programmer a bit lazy ;) Usually in C++ programs such lifetime bungling mistakes, fortunately, lead to a crash (of course, can't depend on it). Is there a PySide option to let Qt crash and burn early on if I made such mistakes instead of the `QCompleter` silently not showing any completion suggestions? – legends2k Jul 18 '23 at 07:51

2 Answers2

1

Elementary mono and bi-dimensional models often rely just on the row and column of the given QModelIndex.

A proper, accurate and safety reliable model should theoretically ensure that the QModelIndex's model() is actually the same, otherwise return an invalid result (None in Python).

Your example works for QSqlQueryModel because SQL models are 2D by their nature, so the assumption is that the given index actually belongs to the same model and then it will try to return the model data based on those row/column coordinates.

This is theoretically a bug, but if we also consider that SQL models often contain thousands of records, it has probably been done for optimization reasons: in my opinion, this is a clear case in which "ask forgiveness, not permission" is better than the opposite.

Your example would have also worked if you used a basic QAbstractTableModel that uses a list of lists as data model and a basic row/column combination to get its values:

class MyModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        if self._data:
            return len(self._data[0])
        return 0

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            return self._data[index.row()][index.column()]

Since the QIdentityProxyModel leaves the original model layout unchanged, the row/column combination will always be consistent with the returned data, even if the model of the QModelIndex is not really the same.

If you had used a QSortFilterProxyModel with filtering active instead, that would have probably returned unexpected results.

A proper and extensible model should always ensure that the model of the QModelIndex actually is the same: QStandardItemModel actually enforces that because it uses internal pointers to reference any item within its structure, and that's necessary since it also allows the possibility of tree structures, which expect a parent item (index) in order to properly map a row and column combination. It retrieves the data not just based on the row/column combination, but based on the parent and the actual model-index ownership.

That's the whole purpose of mapToSource() of proxy models: it returns a (possibly valid) index that actually maps to the source model with the correct index belonging to that model only.

So, the correct implementation of data() (or any index-related function, such as flags()) in a proxy model requires a call to that function, no matter any assumption made on the source model implementation:

class ZeroPrefixProxy(QIdentityProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    # Commenting this method out makes things work
    def data(self, index, role=Qt.DisplayRole):
        return self.sourceModel().data(self.mapToSource(index), role)
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks for imparting this. I'm glad `QStandardItemModel` didn't just work when I missed using `mapToSource`, otherwise I'd not have learned this lesson and will go around thinking it's not needed in identity or other trivial cases. Assumptions/Dependence on a class' implementation details is almost always wrong, it's better to be strict and use `mapToSource` always. – legends2k Jul 18 '23 at 07:59
1

In your MCVE, the QSqlQueryModel + proxy case doesn't work because the source model is destroyed before it's used by the proxy model.

Qt is a C++ framework and hence object lifetimes are to be carefully managed. According to Object Trees & Ownership - Qt documentation, QObjects (from which all Qt objects derive1) live in a tree. Any object can be attached to another as a child using QObject.setParent. The children, grandchildren and descendants are alive as long as the parent is.

What about the root/parent object? When an object has no parent, it's kept alive by the variable it's bound to. As long as the variable is in-scope the object is alive. In your MVCE, window variable holding the MainWindow object is a good example of this; it's in-scope throughout the program's life time irrespective of the many functions/methods the program might end up calling.

When the variable goes out of scope, the object it's holding gets deleted too. This is expected, as according to the compiler, there's no need to hold on to a value, when its reference, the variable goes out of scope, as the value can't be referred-to again by the programmer.

This is the case in your MCVE's model = QSqlQueryModel() variable (whose initializer is missing to pass in parent argument); model is local to MainWindow.__init__ method. When the method ends, model goes out of scope and QSqlQueryModel object gets destroyed. Why does the QStandardItemModel live on then? Well, it's initializer takes in a parent (third argument) which is correctly fed as the MainWindow which is alive all along. Setting the parent to QSqlQueryModel(self) fixes this issue because the model is parented to MainWindow and it'll live until the window is destroyed.

The point that it works sometimes due to calls like model.data(), etc. is just sheer luck. Refer Undefined, unspecified and implementation-defined behavior for details why ;)

1: a universal base

legends2k
  • 31,634
  • 25
  • 118
  • 222