Python Forum

Full Version: File system representation in a data structure
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
I have a problem related to a Qt app, but I figured it would be ok to post it in this section as is it not Qt specific. Still, I left the code in it's real context for the sake of the explanation. I need to build a QMenu that respect the structure of a file system. For example, the files in the 'files' variable below should output a menu like this:

Quote:folderless file
collision > bubu
sub 1 > action1
-------> sub2 > action2
-----------------> collision > action3

I must store the submenus in a dict, and as a key I used the folder name. Obviously, the problem is that there are collisions when a two subfolders share the same name (here 'collision'). Their content is merged like so;

Quote:folderless file
collision > bubu, action3
sub 1 > action1
-------> sub2 > action2
-----------------> collision > bubu, action3

I would like to find a simple way to do this, while supporting nested files. I tried replacing the value 'ref' to make it more specific, but often the result are strange and difficult to debug. Hopefully someone will think of something clever to solve this.

The relevant part is in the 'refresh' function below;

#!/usr/bin/python3
import sys
from pathlib import Path, PurePath
from PyQt5 import QtWidgets, QtCore, QtGui


class Menu(QtWidgets.QMenu):
    def __init__(self, parent):
        super().__init__()
        self.parent = parent
        self.aboutToShow.connect(self.refresh)

    def refresh(self):
        ### For the sake of the example
        CFG_DIR = Path('/app/')
        files = \
            [Path('/app/notes/folderless file.txt'),
            Path('/app/notes/collision/bubu.txt'),
            Path('/app/notes/sub1/action1.txt'),
            Path('/app/notes/sub1/sub2/action2.txt'),
            Path('/app/notes/sub1/sub2/collision/action3.txt')]
        ###

        self.subMenus = {}
        self.items = {}
        menus = set()

        self.clear()
        for path in files:
            name = path.stem
            folders = path.relative_to(CFG_DIR / "notes")
            folders = list(PurePath(folders).parts)[:-1]

            if not folders:
                self.items[path] = QtWidgets.QAction(name, checkable=True)
                self.addAction(self.items[path])

            elif folders[0] != ".trash":
                for i, f in enumerate(reversed(folders), 0):
                    ref = f ##

                    if ref not in self.subMenus:
                        self.subMenus[ref] = QtWidgets.QMenu(f)

                    if i == 0:
                        self.items[path] = QtWidgets.QAction(name, checkable=True)
                        self.subMenus[ref].addAction(self.items[path])
                    else:
                        self.subMenus[ref].addMenu(lastMenu)

                    if i == len(folders)-1:
                        menus.add(self.subMenus[ref])
                    else:
                        lastMenu = self.subMenus[ref]

        for m in menus:
            self.addMenu(m)


class Main(QtWidgets.QSystemTrayIcon):
    def __init__(self, parent):
        super().__init__(parent)
        self.menu = Menu(self)
        self.setContextMenu(self.menu)
        self.show()


def main():
    app = QtWidgets.QApplication(sys.argv)
    widget = Main(app)
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
I could get it to work by first copying the folder structure, then inserting each files in the proper nest. The result is a dict, which is then converted into a QMenu/QAction structure with a resursive fonction. All items are stored in a set to survive the garbage collection.

Quote:# First loop:
{'collision': {}, 'sub1': {'sub2': {'collision': {}}}}

# Second loop:
{'collision': {'collision': '/home/user/.config/qtpad-vim/notes/collision/collision.txt'}, 'sub1': {'sub2': {'collision': {'action3': '/home/user/.config/qtpad-vim/notes/sub1/sub2/collision/action3.txt'}, 'action2': '/home/user/.config/qtpad-vim/notes/sub1/sub2/action2.txt'}, 'action1': '/home/user/.config/qtpad-vim/notes/sub1/action1.txt'}, 'lvl0 with spaces': '/home/user/.config/qtpad-vim/notes/lvl0 with spaces.txt'}

    def _fill(self, current, dest):
        for part in current:
            if type(current[part]) is dict:
                item = QtWidgets.QMenu(part)
                dest.addMenu(item)
                self._fill(current=current[part], dest=item)
            else:
                item = QtWidgets.QAction(part, checkable=True)
                item.triggered.connect(lambda checked, path=current[part]: print(path))
                dest.addAction(item)
            self.items.add(item)

    def refresh(self):
        folder = Path(CFG_DIR / "notes")
        dirs = [x for x in folder.rglob("*") if x.is_dir()]
        files = [x for x in folder.rglob("*.txt") if x.is_file()]
        struct = {}

        for path in dirs:
            folders = path.relative_to(CFG_DIR / "notes")
            folders = PurePath(folders).parts
            last = struct
            for f in folders:
                last.setdefault(f, {})
                last = last[f]

        for path in files:
            folders = path.relative_to(CFG_DIR / "notes")
            folders = PurePath(folders).parts[:-1]
            last = struct
            for f in folders:
                last = last[f]
            last[path.stem] = str(path)

        self.clear()
        self.items = set()
        self._fill(struct, dest=self)
If someone think of a better way I would still like to know :)