1

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.

BPL
  • 9,632
  • 9
  • 59
  • 117
  • This is really no different to the linked question. If there was a way to force an update for one file, then the same could be done for all files. A very crude and buggy hack would be to do a double rename on the file to force an update. But there is no way to avoid race-conditions when doing that, so I'm not going to recommend it. (PS: calling `setData` attempts to rename the file. But if the name hasn't changed, it will have no affect). – ekhumoro Apr 08 '18 at 18:21

1 Answers1

1

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:

  • You need to set this env.var before initializing the model.
  • This feature comes at the cost of potentially heavy load, though. Cached files will be watched if this env.var is on.
BPL
  • 9,632
  • 9
  • 59
  • 117
  • That's a good find, but it's a shame it isn't properly documented. It also seems to be a rather heavy-weight solution. What is really needed is a slot/method that can explicitly refresh the cache for individual files and/or directories. – ekhumoro Apr 10 '18 at 19:10
  • PS: your answer is wrong to assume that this can be used to specify a particular directory where the files will be watched. The envar is simply a switch that turns on per-file watching for ***any files that are cached***, regardless of the directory they happen to be in. So it only ever makes sense to set the envar *once* to a non-empty value, e.g. `os.environ['QT_FILESYSTEMMODEL_WATCH_FILES'] = '1'`. – ekhumoro Apr 10 '18 at 19:32
  • @ekhumoro I totally agree with you, for my real-world case it's working +/- ok. ie: I've got a widget composed by 2widgets+2models, one widget showing folders the other fetching files from selected dir. Each time i change the current directory i update that env.var so it'll monitor that particular directory... so far it's performing rather well, but it's also true i've just tested dirs of few hundred files. I mean, it's not ideal but coding my own QFileSystemModel proved to be quite time consuming. What I don't really like is the fact QFileSystemModel exposes a very minimal public interface :( – BPL Apr 10 '18 at 19:36
  • @ekhumoro My previous comment related to your first comment. About your second comment, you sure about that? Thanks to point it out, I'll test it later on my machine and I'll update the answer if it's so – BPL Apr 10 '18 at 19:40
  • Yes, I'm 99.9% sure. Setting the value to `'1'` in your script does not affect the behaviour. The qt source code currently only uses that envar in one place, and it does not use the value as anything other than a simple switch (i.e. it does not interpret it as a file path). – ekhumoro Apr 10 '18 at 19:44
  • @ekhumoro Thanks, you were totally right, I had understood incorrectly the behaviour of this feature, my bad, I've tested again and it works accordingly to what you've explained in your previous comment. Do you know how the cache is managed by QFileSystemModel? ie: LRU or similar? Asking you cos if the cache grows with no limit the app will become eventually really slow... and i guess you can't flush the cache, can you? :/ – BPL Apr 10 '18 at 21:56
  • The cache is based on `QFileInfo`, but you'll have to read the source to see exactly how it's done. It isn't at all clear how and when the cache entries are invalidated (but I didn't spend long looking at it). There are no public apis that can be used to manipulate the cache - not even indirectly, AFAICS. – ekhumoro Apr 10 '18 at 22:57