Skip to content

PythonObjectTreeModel

Qt Base Class: QAbstractItemModel

Signature: QAbstractItemModel(self, parent: Optional[PySide6.QtCore.QObject] = None) -> None

Base classes

Name Children Inherits
ColumnItemModel
prettyqt.itemmodels.columnitemmodel

⋔ Inheritance diagram

graph TD
  1473290674000["itemmodels.PythonObjectTreeModel"]
  1473245677312["itemmodels.ColumnItemModel"]
  1473245682192["itemmodels.ColumnItemModelMixin"]
  140713234304496["builtins.object"]
  1473299686192["itemmodels.TreeModel"]
  1473299893104["core.AbstractItemModel"]
  1473299890176["core.AbstractItemModelMixin"]
  1473299815024["core.ObjectMixin"]
  1473289050128["QtCore.QAbstractItemModel"]
  1473288842240["QtCore.QObject"]
  1473291690208["Shiboken.Object"]
  1473245677312 --> 1473290674000
  1473245682192 --> 1473245677312
  140713234304496 --> 1473245682192
  1473299686192 --> 1473245677312
  1473299893104 --> 1473299686192
  1473299890176 --> 1473299893104
  1473299815024 --> 1473299890176
  140713234304496 --> 1473299815024
  1473289050128 --> 1473299893104
  1473288842240 --> 1473289050128
  1473291690208 --> 1473288842240
  140713234304496 --> 1473291690208

🛈 DocStrings

Bases: ColumnItemModel

Source code in prettyqt\itemmodels\pythonobjecttreemodel.py
class PythonObjectTreeModel(itemmodels.ColumnItemModel):
    TreeItem = PythonObjectTreeItem
    IS_RECURSIVE = True
    ICON = "mdi.python"
    SUPPORTS = object
    COLUMNS = [
        NameColumn,
        DescriptionColumn,
        PathColumn,
        StrColumn,
        ReprColumn,
        LengthColumn,
        TypeColumn,
        ClassColumn,
        IdColumn,
        AttributeColumn,
        IsCallableColumn,
        IsRoutineColumn,
        IsBuiltinColumn,
        PredicateColumn,
        ModuleColumn,
        # FileColumn,
        # SourceFileColumn,
    ]

    def __init__(self, obj, parent=None):
        super().__init__(obj, self.COLUMNS, show_root=False, parent=parent)

    @classmethod
    def supports(cls, instance) -> bool:
        return True

    def get_path_for_index(self, index: core.ModelIndex) -> str:
        """Get the path for the object referenced by index.

        ### Example:
        ```
        An.example = {"a": [b, c, {"d": e}]} -> path of e: An.example["a"][2]["d"]
        ```
        """
        # TODO: not used yet, better rework ColumnItemModel first
        treeitem = index.data(constants.USER_ROLE)
        if treeitem is None:
            return
        prev_data = treeitem.obj
        pieces = []
        while (index := index.parent()).isValid():
            treeitem = index.data(constants.USER_ROLE)
            data = treeitem.obj
            match data:
                case Mapping():
                    for k, v in data.items():
                        if v is prev_data:
                            pieces.append(f"[{k!r}]")
                            break
                case Iterable():
                    pieces.append(f"[{data.index(prev_data)}]")
                case _:
                    # or should this be treeitem.obj_name?
                    pieces.append(f".{prev_data.__name__}")
            prev_data = data
        pieces.append(treeitem.obj_name)
        logger.info(pieces)
        return "".join(reversed(pieces))

    def _fetch_object_children(
        self, treeitem: PythonObjectTreeItem
    ) -> list[PythonObjectTreeItem]:
        """Fetch the children of a Python object.

        Returns: list of PythonObjectTreeItems
        """
        obj_children = []
        path_strings = []
        obj = treeitem.obj
        obj_path = treeitem.obj_path
        if isinstance(obj, list | tuple | set | frozenset):
            obj_children = [(str(i), j) for i, j in sorted(enumerate(obj))]
            path_strings = [
                f"{obj_path}[{i[0]}]" if obj_path else i[0] for i in obj_children
            ]
        elif isinstance(obj, Mapping):
            obj_children = list(obj.items())
            path_strings = [
                f"{obj_path}[{item[0]!r}]" if obj_path else item[0]
                for item in obj_children
            ]

        is_attr_list = [False] * len(obj_children)

        # Object attributes
        for attr_name, attr_value in sorted(inspect.getmembers(obj)):
            obj_children.append((attr_name, attr_value))
            path_strings.append(f"{obj_path}.{attr_name}" if obj_path else attr_name)
            is_attr_list.append(True)

        return [
            PythonObjectTreeItem(obj=val, name=name, obj_path=p, is_attribute=is_attr)
            for (name, val), p, is_attr in zip(obj_children, path_strings, is_attr_list)
        ]

    def _aux_refresh_tree(self, tree_index: core.ModelIndex):
        """Refresh the tree nodes recursively, auxiliary.

        If the underlying Python object has been changed, we don't want to delete the old
        tree model and create a new one from scratch because this loses all information
        about which nodes are fetched and expanded. Instead the old tree model is updated.
        Using the difflib from the standard library it is determined for a parent node
        which child nodes should be added or removed. This is done based on the node names
        only, not on the node contents (the underlying Python objects). Testing the
        underlying nodes for equality is potentially slow. It is faster to let the
        refreshNode function emit the dataChanged signal for all cells.
        """
        tree_item = self.data_by_index(tree_index)
        if not tree_item.children_fetched:
            return None
        old_items = tree_item.children
        new_items = self._fetch_object_children(tree_item)

        old_item_names = [(item.obj_name, item.is_attribute) for item in old_items]
        new_item_names = [(item.obj_name, item.is_attribute) for item in new_items]
        seq_matcher = SequenceMatcher(
            isjunk=None, a=old_item_names, b=new_item_names, autojunk=False
        )
        opcodes = seq_matcher.get_opcodes()

        logger.debug("(reversed) opcodes: %s", list(reversed(opcodes)))

        for tag, i1, i2, j1, j2 in reversed(opcodes):
            match tag:
                case "equal":
                    # when node names are equal is aux_refresh_tree called recursively.
                    for old_row, new_row in zip(range(i1, i2), range(j1, j2)):
                        old_items[old_row].obj = new_items[new_row].obj
                        child_index = self.index(old_row, 0, parent=tree_index)
                        self._aux_refresh_tree(child_index)

                case "replace":
                    # Remove the old item and insert the new. The old item may have child
                    # nodes which indices must be removed by Qt, otherwise it crashes.
                    first = i1  # row number of first that will be removed
                    last = i1 + i2 - 1  # row number of last element after insertion
                    with self.remove_rows(first, last, tree_index):
                        del tree_item.children[i1:i2]

                    first = i1  # row number of first element after insertion
                    last = i1 + j2 - j1 - 1  # row number of last element after insertion
                    with self.insert_rows(first, last, tree_index):
                        tree_item.insert_children(i1, new_items[j1:j2])

                case "delete":
                    first = i1  # row number of first that will be removed
                    last = i1 + i2 - 1  # row number of last element after insertion
                    with self.remove_rows(first, last, tree_index):
                        del tree_item.children[i1:i2]

                case "insert":
                    first = i1
                    last = i1 + j2 - j1 - 1
                    with self.insert_rows(first, last, tree_index):
                        tree_item.insert_children(i1, new_items[j1:j2])
                case _:
                    raise ValueError(f"Invalid tag: {tag}")

    def refresh_tree(self):
        if self._show_root:
            index = self.createIndex(0, 0, self.inspected_item)
        else:
            index = self.root_index()
        """Refresh the tree model from the underlying root object."""
        self._aux_refresh_tree(index)
        # Emit the dataChanged signal for all cells. This is faster than checking which
        # nodes have changed, which may be slow for some underlying Python objects.
        self.update_all()

get_path_for_index(index: core.ModelIndex) -> str

Get the path for the object referenced by index.

Example:
An.example = {"a": [b, c, {"d": e}]} -> path of e: An.example["a"][2]["d"]
Source code in prettyqt\itemmodels\pythonobjecttreemodel.py
def get_path_for_index(self, index: core.ModelIndex) -> str:
    """Get the path for the object referenced by index.

    ### Example:
    ```
    An.example = {"a": [b, c, {"d": e}]} -> path of e: An.example["a"][2]["d"]
    ```
    """
    # TODO: not used yet, better rework ColumnItemModel first
    treeitem = index.data(constants.USER_ROLE)
    if treeitem is None:
        return
    prev_data = treeitem.obj
    pieces = []
    while (index := index.parent()).isValid():
        treeitem = index.data(constants.USER_ROLE)
        data = treeitem.obj
        match data:
            case Mapping():
                for k, v in data.items():
                    if v is prev_data:
                        pieces.append(f"[{k!r}]")
                        break
            case Iterable():
                pieces.append(f"[{data.index(prev_data)}]")
            case _:
                # or should this be treeitem.obj_name?
                pieces.append(f".{prev_data.__name__}")
        prev_data = data
    pieces.append(treeitem.obj_name)
    logger.info(pieces)
    return "".join(reversed(pieces))

Warning

Model can be recursive, so be careful with iterating whole tree.

Info

Supported data type: <class 'object'>

⌗ Property table

Qt Property Type Doc
objectName QString