Module QFlow.builders.tree
Classes
class Node (path: str, root: str = None, normalRoot: str = None)-
Expand source code
class Node(dict): """ Node of the folder/file tree. The LAST index of the tuple is the one resolved: it always returns the ABSOLUTE path corresponding to that node (folder or file). Special keys, valid only as the FIRST element of the lookup tuple: - 'unf' (unfrozenSpecialKey): rebase the resolved path from the frozen root (`root`) onto the normal, non-frozen root (`normalRoot`) -- i.e. the folder that actually contains the script/executable, instead of a PyInstaller onefile temp extraction folder. - 'f' (frozenSpecialKey): explicitly resolve against the frozen root (`root`), with no rebasing. This is the default behavior when no special key is given at all -- 'f' exists only to let callers be explicit about which resolution they want, symmetric with 'unf'. Everything after the special key (if any) is resolved exactly as usual. """ def __init__(self, path: str, root: str = None, normalRoot: str = None): super().__init__() self.path = path self.root = root if root is not None else path self.normalRoot = normalRoot if normalRoot is not None else path self.unfrozenSpecialKey = 'unf' self.frozenSpecialKey = 'f' def __getitem__(self, key): keys = key if isinstance(key, tuple) else (key,) useNormal = False if keys and keys[0] in (self.unfrozenSpecialKey, self.frozenSpecialKey): useNormal = keys[0] == self.unfrozenSpecialKey keys = keys[1:] node = self for k in keys: node = dict.__getitem__(node, k) path = node.path if isinstance(node, Node) else node if useNormal: relative = os.path.relpath(path, self.root) path = os.path.normpath(os.path.join(self.normalRoot, relative)) return path def __repr__(self): return f'Node(path={self.path!r}, children={list(self.keys())})'Node of the folder/file tree.
The LAST index of the tuple is the one resolved: it always returns the ABSOLUTE path corresponding to that node (folder or file).
Special keys, valid only as the FIRST element of the lookup tuple: - 'unf' (unfrozenSpecialKey): rebase the resolved path from the frozen root (
root) onto the normal, non-frozen root (normalRoot) – i.e. the folder that actually contains the script/executable, instead of a PyInstaller onefile temp extraction folder. - 'f' (frozenSpecialKey): explicitly resolve against the frozen root (root), with no rebasing. This is the default behavior when no special key is given at all – 'f' exists only to let callers be explicit about which resolution they want, symmetric with 'unf'.Everything after the special key (if any) is resolved exactly as usual.
Ancestors
- builtins.dict
class Tree (configFile: str = 'config.json')-
Expand source code
class Tree: """ Tree that scans the folders/patterns defined in a JSON config file and exposes their absolute paths through: tree.f['folder', 'subfolder', ...] Prefix the key with 'unf' to resolve the SAME relative path against the normal (non-frozen) base path instead of the frozen one: tree.f['unf', 'folder', 'subfolder', ...] The config JSON must have the form: { "folders": ["app", "app/**/*.html"] } Patterns are resolved with glob (supports recursive '**') relative to the project's base path (see `basePath`), which changes automatically if the program is compiled/frozen. """ def __init__(self, configFile: str = 'config.json'): self.root = Tree.basePath() self.normalRoot = Tree.normalPath() cfgPath = ( configFile if os.path.isabs(configFile) else os.path.join(self.root, configFile) ) self.config = self.loadConfig(cfgPath) self.f = self.buildTree(getattr(self.config, 'folders', [])) def loadConfig(self, path: str) -> SimpleNamespace: if not os.path.isfile(path): raise FileNotFoundError(f"Config file not found: '{path}'.") with open(path, 'r', encoding='utf-8') as fh: data = json.load(fh) return Object(data).obj def buildTree(self, patterns) -> Node: root = Node(self.root, root=self.root, normalRoot=self.normalRoot) for pattern in patterns: patternPath = os.path.join(self.root, pattern) matches = glob.glob(patternPath, recursive=True) if not matches: matches = [patternPath] for absolutePath in matches: relative = os.path.relpath(absolutePath, self.root) parts = relative.split(os.sep) self.insertNode(root, parts, os.path.abspath(absolutePath)) return root def insertNode(self, node: Node, parts: list, absolutePath: str): current = node accumulated = self.root for part in parts: accumulated = os.path.join(accumulated, part) if part not in current: dict.__setitem__(current, part, Node(os.path.abspath(accumulated))) current = dict.__getitem__(current, part) current.path = absolutePath @staticmethod def basePath() -> str: """ Absolute base path of the project. - If the executable is frozen (PyInstaller or another bundler): * sys._MEIPASS -> --onefile mode (resources extracted to a temp folder) * dirname(sys.executable) -> --onedir mode (resources next to the binary) - If running as a normal script, uses the directory of the script that started the process (sys.argv[0]). """ if FROZENLIB: return os.path.abspath(sys._MEIPASS) return os.path.dirname(os.path.abspath(sys.argv[0])) @staticmethod def normalPath() -> str: """ Absolute path of the folder that actually contains the running script/executable, regardless of freezing mode. Unlike `basePath`, this NEVER returns a --onefile temp extraction folder (sys._MEIPASS): it always points to where the .exe/script physically lives, which is what you want for files that must be read/written alongside the binary (logs, user config, output, etc.) rather than looked up inside the bundled resources. """ if FROZENLIB: return os.path.dirname(os.path.abspath(sys.executable)) return os.path.dirname(os.path.abspath(sys.argv[0]))Tree that scans the folders/patterns defined in a JSON config file and exposes their absolute paths through:
tree.f['folder', 'subfolder', ...]Prefix the key with 'unf' to resolve the SAME relative path against the normal (non-frozen) base path instead of the frozen one:
tree.f['unf', 'folder', 'subfolder', ...]The config JSON must have the form: { "folders": ["app", "app/*/.html"] }
Patterns are resolved with glob (supports recursive '**') relative to the project's base path (see
basePath), which changes automatically if the program is compiled/frozen.Static methods
def basePath() ‑> str-
Expand source code
@staticmethod def basePath() -> str: """ Absolute base path of the project. - If the executable is frozen (PyInstaller or another bundler): * sys._MEIPASS -> --onefile mode (resources extracted to a temp folder) * dirname(sys.executable) -> --onedir mode (resources next to the binary) - If running as a normal script, uses the directory of the script that started the process (sys.argv[0]). """ if FROZENLIB: return os.path.abspath(sys._MEIPASS) return os.path.dirname(os.path.abspath(sys.argv[0]))Absolute base path of the project.
- If the executable is frozen (PyInstaller or another bundler):
- sys._MEIPASS -> –onefile mode (resources extracted to a temp folder)
- dirname(sys.executable) -> –onedir mode (resources next to the binary)
- If running as a normal script, uses the directory of the script that started the process (sys.argv[0]).
- If the executable is frozen (PyInstaller or another bundler):
def normalPath() ‑> str-
Expand source code
@staticmethod def normalPath() -> str: """ Absolute path of the folder that actually contains the running script/executable, regardless of freezing mode. Unlike `basePath`, this NEVER returns a --onefile temp extraction folder (sys._MEIPASS): it always points to where the .exe/script physically lives, which is what you want for files that must be read/written alongside the binary (logs, user config, output, etc.) rather than looked up inside the bundled resources. """ if FROZENLIB: return os.path.dirname(os.path.abspath(sys.executable)) return os.path.dirname(os.path.abspath(sys.argv[0]))Absolute path of the folder that actually contains the running script/executable, regardless of freezing mode.
Unlike
basePath, this NEVER returns a –onefile temp extraction folder (sys._MEIPASS): it always points to where the .exe/script physically lives, which is what you want for files that must be read/written alongside the binary (logs, user config, output, etc.) rather than looked up inside the bundled resources.
Methods
def buildTree(self, patterns) ‑> Node-
Expand source code
def buildTree(self, patterns) -> Node: root = Node(self.root, root=self.root, normalRoot=self.normalRoot) for pattern in patterns: patternPath = os.path.join(self.root, pattern) matches = glob.glob(patternPath, recursive=True) if not matches: matches = [patternPath] for absolutePath in matches: relative = os.path.relpath(absolutePath, self.root) parts = relative.split(os.sep) self.insertNode(root, parts, os.path.abspath(absolutePath)) return root def insertNode(self,
node: Node,
parts: list,
absolutePath: str)-
Expand source code
def insertNode(self, node: Node, parts: list, absolutePath: str): current = node accumulated = self.root for part in parts: accumulated = os.path.join(accumulated, part) if part not in current: dict.__setitem__(current, part, Node(os.path.abspath(accumulated))) current = dict.__getitem__(current, part) current.path = absolutePath def loadConfig(self, path: str) ‑> types.SimpleNamespace-
Expand source code
def loadConfig(self, path: str) -> SimpleNamespace: if not os.path.isfile(path): raise FileNotFoundError(f"Config file not found: '{path}'.") with open(path, 'r', encoding='utf-8') as fh: data = json.load(fh) return Object(data).obj