Re: QTreeView + sort + delete = crash

Kyle Altendorf <[email protected]>
Newsgroups gmane.comp.python.pyqt-pykde
Message-ID <[email protected]>
On 2019-05-04 23:08, John F Sturtz wrote:

>> On 5/3/2019 1:36:04 AM, Florian Bruhin <[email protected]> wrote:
>> 
>> You might want to try running pytest-qt's item model tester over your 
>> model:
>> https://pytest-qt.readthedocs.io/en/latest/modeltester.html
> 
> So, I think I wrapped my mind around pytest and pytest-qt enough to use 
> the model tester coherently.
> 
> If I mock up a file with the simple model shown on the Model Tester 
> documentation site 
> (https://pytest-qt.readthedocs.io/en/latest/modeltester.html) and run 
> pytest on it, I get the output I expect -- it indicates that the test 
> has run and passed (attached file img1.png).
> 
> But the model tester seems to crash on the model I posted previously.  
> The test starts to run but never finishes, and doesn't generate any 
> messages (attached file img2.png).
> 
> I must have really done something egregiously wrong.  :-/
> 
> The modified cat.py is attached (I cleaned it up a bit to make it a 
> little easier to add the tester function).

It's late so I won't promise it's testing properly...  but the attached 
.py runs the test and passes.  I'd note that you shouldn't be passing 
the view to the model.  The intention is that the Qt defined interface 
is all that is needed.  For example, what happens when you have two 
views for one model?

Cheers,
-kyle

_______________________________________________
PyQt mailing list    [email protected]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
x.py (text/x-python, 10.5 KB)
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5 import uic

from itertools import groupby
import sys



# ------------------------------------------------------------------------------
# | CategoryNode                                                               |
# |                                                                            |
# |                                                                            |
# |                                                                            |
# | Defines the nodes of the data tree that underlies CategoryTreeModel        |
# ------------------------------------------------------------------------------
class CategoryNode:
    def __init__(self, desc, parent=None):
        self.parent_node = parent
        self._desc = desc
        self.children = []

    # ----------------------------------------------------------------------
    # | CategoryNode accessor functions
    # ----------------------------------------------------------------------

    # Number of columns is always one in this model
    @staticmethod
    def n_columns():
        return 1

    # Return category description
    def desc(self):
        return self._desc

    # Set category description
    def set_desc(self, desc):
        self._desc = desc

    # Return row
    def row(self):
        if self.parent_node:
            return self.parent_node.children.index(self)
        else:
            return 0

    # Return parent
    def parent(self):
        return self.parent_node

    # Add a child
    def append_child(self, c):
        self.children.append(c)

    # Return child from given row
    def child(self, row):
        return self.children[row]

    # Return number of children
    def n_children(self):
        return len(self.children)

    # Sort children
    def sort_children(self):
        self.children.sort(key=lambda x: x.desc())
        return

    # Delete child from given row
    def del_child(self, row):
        del self.children[row]

    # Printable representation
    def __repr__(self):
        return str(f'->{self._desc}')

    # Recursively dump tree contents
    def dump(self, i=0):
        print(f'''{(' ' * (i * 4))} {self.row()} -> {self}''')
        for c in self.children:
            c.dump(i+1)


# ------------------------------------------------------------------------------
# | CategoryTreeModel                                                          |
# |                                                                            |
# |                                                                            |
# |                                                                            |
# | Model used by CategoryDialog tree view                                     |
# ------------------------------------------------------------------------------
class CategoryTreeModel(QtCore.QAbstractItemModel):

    # ------------------------------------------------------------[override]
    # | __init__()
    # ----------------------------------------------------------------------
    def __init__(self, parent=None):
        super().__init__(parent)

        category_data = [
            (4, 'Auto', None), (10, 'Auto', 'Gas'), (9, 'Auto', 'License'),
                (11, 'Auto', 'Service'),
            (23, 'Beer', None),
            (12, 'Clothing', None), (13, 'Clothing', 'Cleaning'),
            (18, 'Dining', None), (19, 'Dining', 'Breakfast'), (21, 'Dining', 'Lunch'),
            (14, 'Healthcare', None), (26, 'Healthcare', 'Dental'),
            (3, 'Household', None),
            (5, 'Pet Expenses', None), (7, 'Pet Expenses', 'Biskies'),
                (17, 'Pet Expenses', 'Chow'), (8, 'Pet Expenses', 'Grooming'),
                (6, 'Pet Expenses', 'Medical'), (15, 'Pet Expenses', 'Miscellaneous'),
            (25, 'Reimbursible', None)
        ]

        # Create root node
        self.root = CategoryNode('Category')

        # Create main category/subcategory node structure
        for g in groupby(category_data, lambda rec: rec[1]):
            sub_list = list(g[1])
            main_node = CategoryNode(g[0], self.root)
            self.root.append_child(main_node)
            for s in sub_list:
                if s[2] is not None:
                    sub_node = CategoryNode(s[2], main_node)
                    main_node.append_child(sub_node)

    # ------------------------------------------------------------[override]
    # | rowCount()
    # ----------------------------------------------------------------------
    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return parent.internalPointer().n_children()
        else:
            return self.root.n_children()

    # ------------------------------------------------------------[override]
    # | columnCount()
    # ----------------------------------------------------------------------
    def columnCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return parent.internalPointer().n_columns()
        else:
            return self.root.n_columns()

    # ------------------------------------------------------------[override]
    # | data()
    # ----------------------------------------------------------------------
    def data(self, index, role=QtCore.Qt.DisplayRole):
        if index.isValid():
            if role == QtCore.Qt.DisplayRole:
                return index.internalPointer().desc()
            elif role == QtCore.Qt.FontRole:
                font = QtGui.QFont('Candara')
                font.setPointSize(11)
                return font
            elif role == QtCore.Qt.ForegroundRole:
                color = '#1e62ce'
                return QtGui.QBrush(QtGui.QColor(color))

        return None

    # ------------------------------------------------------------[override]
    # | flags()
    # ----------------------------------------------------------------------
    def flags(self, index):
        if index.isValid():
            return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
        else:
            return QtCore.Qt.NoItemFlags

    # ------------------------------------------------------------[override]
    # | index()
    # ----------------------------------------------------------------------
    def index(self, row, column, parent=QtCore.QModelIndex()):

        # According to legend, index() may sometimes be called with invalid parameters
        # (that is, with parameters that don't actually have an index).  So this is
        # necessary to keep it from crashing.
        # https://stackoverflow.com/questions/26680168
        #     /pyqt-treeview-index-error-removing-last-row
        if not self.hasIndex(row, column, parent):
            return QtCore.QModelIndex()

        elif parent.isValid():
            return self.createIndex(row, column, parent.internalPointer().child(row))
        else:
            return self.createIndex(row, column, self.root.child(row))

    # ------------------------------------------------------------[override]
    # | parent()
    # ----------------------------------------------------------------------
    def parent(self, index):
        if not index.isValid():
            return QtCore.QModelIndex()

#       print(f'parent(), before index.internalPointer() call - row={index.row()}')
        child_node = index.internalPointer()
#       print(f'parent(), after index.internalPointer() call  - child_node={child_node}')
        parent_node = child_node.parent()
        if parent_node == self.root:
            r = QtCore.QModelIndex()
        else:
            r = self.createIndex(parent_node.row(), 0, parent_node)
        return r


    # ------------------------------------------------------------[override]
    # | setData()
    # ----------------------------------------------------------------------
    # noinspection PyUnresolvedReferences
    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:

            # Set value in data tree node
            node = index.internalPointer()
            node.set_desc(value)

            # Re-sort at the level of the modified node (i.e., the children of the
            # modified node's parent)
            # Need to emit layoutAboutToBeChanged and layoutChanged signals to get
            # QTreeView to update properly
            self.layoutAboutToBeChanged.emit([], QtCore.QAbstractItemModel.VerticalSortHint)
            node.parent().sort_children()
            self.layoutChanged.emit([], QtCore.QAbstractItemModel.VerticalSortHint)

            # Make modified item current
            parent = self.parent(index)
            self.view.setCurrentIndex(self.index(node.row(), 0, parent))
            self.view.setFocus(QtCore.Qt.OtherFocusReason)


    # ------------------------------------------------------------[override]
    # | insertRows()
    # ----------------------------------------------------------------------
    def insertRows(self, row, rows, parent=QtCore.QModelIndex()):
        assert rows == 1

        # Determine parent
        if parent.internalPointer() is None:
            parent_node = self.root
        else:
            parent_node = parent.internalPointer()

        # Create new data tree node and add it to parent
        new = CategoryNode(None, parent_node)
        self.beginInsertRows(parent, row, row + rows - 1)
        parent_node.append_child(new)
        self.endInsertRows()

        return True

    # ------------------------------------------------------------[override]
    # | removeRows()
    # ----------------------------------------------------------------------
    def removeRows(self, row, rows, parent=QtCore.QModelIndex()):
        assert rows == 1

        # Determine parent
        if parent.internalPointer() is None:
            parent_node = self.root
        else:
            parent_node = parent.internalPointer()

        # Delete it from the parent
        self.beginRemoveRows(parent, row, row + rows - 1)
        parent_node.del_child(row)
        self.endRemoveRows()

        return True

# ------------------------------------------------------------------------------
# | pytest tester function                                                     |
# |                                                                            |
# |                                                                            |
# |                                                                            |
# |                                                                            |
# ------------------------------------------------------------------------------
def test_model(qtmodeltester):
    qtmodeltester.check(CategoryTreeModel())
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.