pythonpyqtpyqt5qfilesystemmodelqfilesystemwatcher

How to update file info in a QFileSystemModel?


First of all, this question is similar to this other one QFileSystemModel not updating when files change with the main difference than in this case I don't want to replace the entire model each time one of my files are updated.

In the real world example my app will have opened few files, so I'm basically just trying to understand how to update the info (size, date modified) of one particular QFileSystemModel item, below you have a little mcve to play with, as you can see in that code I've unsuccesfully tried using setData:

import sys
import os

from PyQt5.Qt import *  # noqa


class DirectoryTreeWidget(QTreeView):

    def __init__(self, path=QDir.currentPath(), *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.init_model(path)
        self.expandsOnDoubleClick = False
        self.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
        self.setAutoScroll(True)

    def init_model(self, path):
        self.extensions = ["*.*"]
        self.path = path
        model = QFileSystemModel(self)
        model.setRootPath(QDir.rootPath())
        model.setReadOnly(False)
        model.setFilter(QDir.AllDirs | QDir.NoDot | QDir.AllEntries)
        self.setModel(model)
        self.set_path(path)

    def set_path(self, path):
        self.path = path
        model = self.model()
        index = model.index(str(self.path))

        if os.path.isfile(path):
            self.setRootIndex(model.index(
                os.path.dirname(str(self.path))))
            self.scrollTo(index)
            self.setCurrentIndex(index)
        else:
            self.setRootIndex(index)


class Foo(QWidget):

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

        self.path = path

        self.tree_view = DirectoryTreeWidget(path=".")
        self.tree_view.show()
        bt = QPushButton(f"Update {path}")
        bt.pressed.connect(self.update_file)

        layout = QVBoxLayout()
        layout.addWidget(self.tree_view)
        layout.addWidget(bt)

        self.setLayout(layout)

        # New file will automatically refresh QFileSystemModel
        self.create_file()

    def create_file(self):
        with open(self.path, "w") as f:
            data = "This new file contains xx bytes"
            f.write(data.replace("xx", str(len(data))))

    def update_file(self):
        model = self.tree_view.model()

        # Updating a file won't refresh QFileSystemModel, the question is,
        # how can we update that particular item to be refreshed?
        data = "The file updated is much larger, it contains xx bytes"
        with open(self.path, "w") as f:
            f.write(data.replace("xx", str(len(data))))

        # file_info = self.obj.model.fileInfo(index)
        # file_info.refresh()
        index = model.index(self.path)
        model.setData(index, model.data(index))
        QMessageBox.about(None, "Info", f"{self.path} updated, new size is {len(data)}")


def main():
    app = QApplication(sys.argv)
    foo = Foo("foo.txt")
    foo.setMinimumSize(640, 480)
    foo.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

So the question would be, how can I achieve that update_file updates the info of that particular file foo.txt?

The goal would be updating just that file wihout replacing the entire model like shown here, also once that model item is updated it'd be nice the item is not sorted/moved in the view at all.


Solution

  • Qt v5.9.4 has introduced the environment variable QT_FILESYSTEMMODEL_WATCH_FILES in order to address QTBUG-46684, you can read more about it in the changelog:

    QTBUG-46684 It is now possible to enable per-file watching by setting the environment variable QT_FILESYSTEMMODEL_WATCH_FILES, allowing to track for example changes in file size.

    So to make the example working you can just set the envar once to a non-empty value, ie:

    import sys
    import os
    
    from PyQt5.Qt import *  # noqa
    
    
    class DirectoryTreeWidget(QTreeView):
    
        def __init__(self, path=QDir.currentPath(), *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            self.init_model(path)
            self.expandsOnDoubleClick = False
            self.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
            self.setAutoScroll(True)
    
        def init_model(self, path):
            os.environ["QT_FILESYSTEMMODEL_WATCH_FILES"] = '1'
    
            self.extensions = ["*.*"]
            self.path = path
            model = QFileSystemModel(self)
            model.setRootPath(QDir.rootPath())
            model.setReadOnly(False)
            model.setFilter(QDir.AllDirs | QDir.NoDot | QDir.AllEntries)
            self.setModel(model)
            self.set_path(path)
    
        def set_path(self, path):
            self.path = path
            model = self.model()
            index = model.index(str(self.path))
    
            if os.path.isfile(path):
                self.setRootIndex(model.index(
                    os.path.dirname(str(self.path))))
                self.scrollTo(index)
                self.setCurrentIndex(index)
            else:
                self.setRootIndex(index)
    
    
    class Foo(QWidget):
    
        def __init__(self, path):
            super().__init__()
    
            self.path = path
    
            self.tree_view = DirectoryTreeWidget(path=".")
            self.tree_view.show()
            bt = QPushButton(f"Update {path}")
            bt.pressed.connect(self.update_file)
    
            layout = QVBoxLayout()
            layout.addWidget(self.tree_view)
            layout.addWidget(bt)
    
            self.setLayout(layout)
            self.create_file()
    
        def create_file(self):
            with open(self.path, "w") as f:
                data = "This new file contains xx bytes"
                f.write(data.replace("xx", str(len(data))))
    
        def update_file(self):
            model = self.tree_view.model()
    
            data = "The file updated is much larger, it contains xx bytes"
            with open(self.path, "w") as f:
                f.write(data.replace("xx", str(len(data))))
    
            index = model.index(self.path)
            model.setData(index, model.data(index))
            QMessageBox.about(None, "Info", f"{self.path} updated, new size is {len(data)}")
    
    
    def main():
        app = QApplication(sys.argv)
        foo = Foo("foo.txt")
        foo.setMinimumSize(640, 480)
        foo.show()
        sys.exit(app.exec_())
    
    
    if __name__ == "__main__":
        main()
    

    Couple of comments: