QTreeView, QAbstractItemModel. Падение при раскрытии узла модели

Нужно отобразить в QTreeView древовидную модель. Дочерние узлы должны создаваться при раскрытии узла пользователем. Количество дочерних узлов заранее неизвестно. Некоторые дочерние узлы могут быть созданы сразу после раскрытия родительского узла. О некоторых дочерних узлах необходимо запросить данные и только после получения этих данных они могут быть созданы.

Для этого создан бутерброд QTreeView + Прокси-модель для сортировки и фильтрации + Модель Qt (наследник QAbstractItemModel) + Модель данных. Код ниже отлично работает при наличии прокси-модели. При ее отсутствии попытка раскрыть узел приводит к падению: Process finished with exit code -1073741819 (0xC0000005) Боюсь, что это признак ошибки, которая рано или поздно проявится и при наличии прокси-модели.

Подскажите, в чем может быть причина/на что стоит обратить внимание?

UPD 1: добавлен сигнал _populate_request с QueuedConnection, чтобы "отделить" стек вызовов fetchMore от добавления/удаления узлов из модели. Это работает, но идея по-прежнему не кажется мне очевидной. И нет уверенности, что это самое верное решение. Какие-то комментарии или выдержки из документации помогли бы.

UPD 2: добавлено контекстное меню "Reload" для перезагрузки дочерних узлов. Чтобы проверить удаление старых узлов из модели.

import random
import sys
import weakref
from enum import Enum, auto
from typing import Optional, List
from PyQt5 import QtCore, QtTest
from PyQt5.QtWidgets import QMainWindow, QTreeView, QVBoxLayout, QApplication, QMenu
from PyQt5.QtCore import QModelIndex, Qt


class TreeNodeStatus(Enum):
    NotPopulated = auto()
    Populating = auto()
    Populated = auto()
    Error = auto()


class TreeNode(QtCore.QObject):
    """ Узел дерева объектов; корневой узел по сути и есть модель данных """

    status_changed = QtCore.pyqtSignal(object)  # node
    before_children_added = QtCore.pyqtSignal(object, int, int)  # parent_node, pos, count
    children_added = QtCore.pyqtSignal(object, int, int)  # parent_node, pos, count
    before_children_removed = QtCore.pyqtSignal(object, int, int)  # parent_node, pos, count
    children_removed = QtCore.pyqtSignal(object, int, int)  # parent_node, pos, count

    _populate_request = QtCore.pyqtSignal()

    def __init__(self, name: str, parent: Optional['TreeNode']):
        super().__init__()

        self._name = name

        self._parent_ref = weakref.ref(parent) if parent is not None else lambda: None

        self._status: TreeNodeStatus = TreeNodeStatus.NotPopulated

        self._children: List[TreeNode] = []

        # чтобы следить за сигналами только корневого узла
        if parent is not None:
            self.status_changed.connect(parent.status_changed)
            self.before_children_added.connect(parent.before_children_added)
            self.children_added.connect(parent.children_added)
            self.before_children_removed.connect(parent.before_children_removed)
            self.children_removed.connect(parent.children_removed)

        # чтобы отделить стек вызовов fetchMore > populate от добавления/удаления узлов из модели;
        # для узлов, которые создаются сразу в populate, прямой вызов
        # fetchMore > populate > _on_children_received приводит к падению;
        # при вызове populate с помощью этого сигнала работает без падений
        self._populate_request.connect(self._populate, Qt.ConnectionType.QueuedConnection)

        # для узлов, которые создаются с задержкой, только после получения данных;
        # чтобы имитировать задержку получения данных
        self._timer = QtCore.QTimer()
        self._timer.setSingleShot(True)
        self._timer.setInterval(2 * 1000)  # 2с
        self._timer.timeout.connect(self._on_children_received)

    def parent(self) -> Optional['TreeNode']:
        return self._parent_ref()

    @property
    def status(self) -> TreeNodeStatus:
        return self._status

    def _set_status(self, status: TreeNodeStatus):
        self._status = status
        self.status_changed.emit(self)

    def populate(self):
        # # вызов через сигнал с QueuedConnection - работает
        # self._populate_request.emit()
        # прямой вызов приводит к падению для узлов, создающихся сразу, если нет прокси-модели
        self._populate()

    def _populate(self):

        if self.status == TreeNodeStatus.Populating:
            return

        self._set_status(TreeNodeStatus.Populating)

        # забываем старых детей
        old_children_count = len(self._children)
        self.before_children_removed.emit(self, 0, old_children_count)
        # disconnect signals
        for child in self._children:
            child.status_changed.disconnect(self.status_changed)
            child.before_children_added.disconnect(self.before_children_added)
            child.children_added.disconnect(self.children_added)
            child.before_children_removed.disconnect(self.before_children_removed)
            child.children_removed.disconnect(self.children_removed)
        self._children.clear()
        self.children_removed.emit(self, 0, old_children_count)

        # запрашиваем данные о дочерних узлах
        # # таймер - для узлов, которые требуют запроса данных
        # self._timer.start()
        # прямой вызов - для узлов, которые можем создать сразу
        self._on_children_received()

    def children(self) -> List['TreeNode']:
        return self._children

    @property
    def name(self) -> str:
        return self._name

    def _on_children_received(self):
        print('!_on_children_received', self.name)

        # создаем дочерние узлы
        new_children_count = random.randint(0, 4)
        self.before_children_added.emit(self, 0, new_children_count)
        self._children = [TreeNode(self.name + ' ' + str(i), self) for i in range(new_children_count)]
        self.children_added.emit(self, 0, new_children_count)

        self._set_status(TreeNodeStatus.Populated)


class TreeModel(QtCore.QAbstractItemModel):

    def __init__(self, root_node: TreeNode):
        super().__init__()

        # root node == data model
        self._root_node = root_node
        self._root_node.status_changed.connect(self._on_node_status_changed)
        self._root_node.before_children_added.connect(self._before_children_added)
        self._root_node.children_added.connect(self._on_children_added)
        self._root_node.before_children_removed.connect(self._before_children_removed)
        self._root_node.children_removed.connect(self._on_children_removed)

    def index(self, row: int, column: int, parent=QModelIndex(), *args, **kwargs) -> QModelIndex:
        # отбрасываем несуществующие индексы: внутри проверки на row/column для данного parent
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        # получим родительский узел по индексу
        if parent is None or not parent.isValid():
            parent_node: TreeNode = self._root_node
        else:
            parent_node: TreeNode = parent.internalPointer()

        # если есть ребенок с данных номером строки
        if row < len(parent_node.children()):
            # создаем индекс с узлом в качестве internalPointer
            return self.createIndex(row, column, parent_node.children()[row])

        return QModelIndex()

    def parent(self, index: QModelIndex = None) -> QModelIndex:
        # invalid index => root node
        if not index.isValid():
            return QModelIndex()

        node: TreeNode = index.internalPointer()
        parent_node: TreeNode = node.parent()

        # если искомый родитель - корень, возвращаем невалидный индекс
        if parent_node is self._root_node:
            return QModelIndex()

        # получим row для родительского узла; parent_node - не корень, гарантировано имеет собственного родителя
        grandparent_node = parent_node.parent()
        parent_row = grandparent_node.children().index(parent_node)

        # создаем индекс с узлом в качестве internalPointer
        return self.createIndex(parent_row, 0, parent_node)

    def hasChildren(self, parent=QModelIndex(),  *args, **kwargs) -> bool:
        # можем ли мы развернуть узел? если да, слева от узла отображается треугольник

        parent_node = self._node_from_index(parent)

        # дети загружены - смотрим количество
        if parent_node.status == TreeNodeStatus.Populated:
            return len(parent_node.children()) > 0
        # error - детей нет, развернуть нельзя
        elif parent_node.status == TreeNodeStatus.Error:
            return False
        # дети не загружены/загружаются - предполагаем, что они есть
        else:
            return True

    def canFetchMore(self, parent: QModelIndex) -> bool:
        # можем ли мы получить больше данных (дочерних узлов) для parent?
        # print('canFetchMore!', self._node_from_index(parent).name)
        return self._can_fetch_more(parent)

    def _can_fetch_more(self, parent: QModelIndex) -> bool:
        parent_node = self._node_from_index(parent)

        # дети не загружены - предполагаем, что они есть
        if parent_node.status == TreeNodeStatus.NotPopulated:
            return True
        # в других случаях больше данных получить нельзя
        elif parent_node.status in [TreeNodeStatus.Populating,
                                    TreeNodeStatus.Populated,
                                    TreeNodeStatus.Error]:
            return False
        assert False

    def fetchMore(self, parent: QModelIndex) -> None:
        # получаем больше данных (дочерних узлов) для parent
        print('!FetchMore', self._node_from_index(parent).name)

        if not self._can_fetch_more(parent):
            return

        parent_node = self._node_from_index(parent)

        if parent_node.status != TreeNodeStatus.Populating:
            parent_node.populate()

    def rowCount(self, parent=QModelIndex(), *args, **kwargs):
        parent_node = self._node_from_index(parent)
        return len(parent_node.children())

    def columnCount(self, parent=None, *args, **kwargs):
        return 1

    def _node_from_index(self, index: Optional[QModelIndex]) -> TreeNode:
        # invalid index - root node
        if index is None or not index.isValid():
            return self._root_node
        else:
            return index.internalPointer()

    def _index_from_node(self, node: TreeNode) -> Optional[QModelIndex]:
        # root node - invalid index
        if node is self._root_node:
            return QModelIndex()

        # по принципу из index
        parent_node = node.parent()
        row = parent_node.children().index(node)
        return self.createIndex(row, 0, node)

    def data(self, index, role=None):

        node = self._node_from_index(index)

        if role == Qt.DisplayRole:
            return node.name

        # узлы - по UserRole
        elif role == Qt.UserRole:
            return node

        elif role == Qt.DecorationRole:
            pass

    def _on_node_status_changed(self, node: TreeNode):
        index = self._index_from_node(node)

        if index is not None:
            # оповестим об изменениях - иконка, тултип
            self.dataChanged.emit(index, index, [Qt.DecorationRole, Qt.ToolTipRole])

    def _before_children_removed(self, parent_node: TreeNode, pos: int, count: int):
        parent_index = self._index_from_node(parent_node)

        if parent_index is not None:
            self.beginRemoveRows(parent_index, pos, pos + count - 1)

    def _on_children_removed(self, parent_node: TreeNode, pos: int, count: int):
        self.endRemoveRows()

    def _before_children_added(self, parent_node: TreeNode, pos: int, count: int):
        parent_index = self._index_from_node(parent_node)

        if parent_index is not None:
            self.beginInsertRows(parent_index, pos, pos + count - 1)
            print('!beginInsertRows', parent_node.name)

    def _on_children_added(self, parent_node: TreeNode, pos: int, count: int):
        self.endInsertRows()
        print('!endInsertRows', parent_node.name)


class TreeView(QTreeView):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._menu = QMenu(self)

        # загрузка детей заново
        self._reload_act = self._menu.addAction('Reload')
        self._reload_act.triggered.connect(self._on_reload)

    def mouseReleaseEvent(self, event):
        """ Вызываем контекстное меню """
        super().mouseReleaseEvent(event)
        if event.button() == Qt.MouseButton.RightButton:
            index = self.indexAt(event.pos())
            # above nodes only
            if index.isValid():
                self._menu.popup(self.viewport().mapToGlobal(event.pos()))

    def _on_reload(self):
        index = self.currentIndex()
        node = index.data(role=Qt.UserRole)
        if node.status != TreeNodeStatus.Populating:
            node.populate()


class ClientWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self._setup_ui()

        root_node = TreeNode('root', None)
        model = TreeModel(root_node)
        # proxy = QtCore.QSortFilterProxyModel()
        # proxy.setSourceModel(model)
        # FixMe падает при раскрытии узла, если передаем сюда исходную модель
        self._view.setModel(model)

    def _setup_ui(self):
        self._view = TreeView()
        self._view.setSortingEnabled(True)

        central_wdg = self._view
        central_vlt = QVBoxLayout()
        central_wdg.setLayout(central_vlt)
        self.setCentralWidget(central_wdg)


if __name__ == '__main__':
    app = QApplication([])
    main_window = ClientWindow()
    main_window.show()
    sys.exit(app.exec())


Ответы (0 шт):