Nymbo commited on
Commit
b3f4dee
·
verified ·
1 Parent(s): 266debf

Create _tree_utils.py

Browse files
Files changed (1) hide show
  1. Modules/_tree_utils.py +265 -0
Modules/_tree_utils.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Shared tree rendering utilities for filesystem-like output.
3
+
4
+ Provides functions to build and render tree structures with line connectors
5
+ (├──, └──, │) for visual hierarchy display.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from datetime import datetime
12
+ from typing import Callable, Optional
13
+
14
+
15
+ def build_tree(entries: list[tuple[str, dict]]) -> dict:
16
+ """
17
+ Build a nested tree structure from flat path entries.
18
+
19
+ Args:
20
+ entries: List of (path, metadata) tuples where path uses forward slashes.
21
+ Paths ending with '/' are treated as directories.
22
+
23
+ Returns:
24
+ Nested dict with "__files__" key for files at each level.
25
+
26
+ Example:
27
+ entries = [
28
+ ("scripts/utils.py", {"size": 1234}),
29
+ ("docs/readme.md", {"size": 500}),
30
+ ]
31
+ tree = build_tree(entries)
32
+ """
33
+ root: dict = {"__files__": []}
34
+
35
+ for path, metadata in entries:
36
+ parts = path.rstrip("/").split("/")
37
+ is_dir = path.endswith("/")
38
+
39
+ node = root
40
+ for i, part in enumerate(parts[:-1]):
41
+ if part not in node:
42
+ node[part] = {"__files__": []}
43
+ node = node[part]
44
+
45
+ final = parts[-1]
46
+ if is_dir:
47
+ # Ensure directory exists
48
+ if final not in node:
49
+ node[final] = {"__files__": []}
50
+ # Store directory metadata if provided
51
+ if metadata:
52
+ node[final]["__meta__"] = metadata
53
+ else:
54
+ # Add file
55
+ node["__files__"].append((final, metadata))
56
+
57
+ return root
58
+
59
+
60
+ def render_tree(
61
+ node: dict,
62
+ prefix: str = "",
63
+ format_entry: Optional[Callable[[str, dict, bool], str]] = None,
64
+ ) -> list[str]:
65
+ """
66
+ Render a tree with line connectors.
67
+
68
+ Args:
69
+ node: Nested dict from build_tree()
70
+ prefix: Current line prefix for indentation
71
+ format_entry: Optional callback to format each entry.
72
+ Signature: (name, metadata, is_dir) -> str
73
+ If None, uses default formatting.
74
+
75
+ Returns:
76
+ List of formatted lines.
77
+ """
78
+ result = []
79
+
80
+ # Default formatter
81
+ def default_format(name: str, meta: dict, is_dir: bool) -> str:
82
+ if is_dir:
83
+ return f"{name}/"
84
+ size = meta.get("size")
85
+ if size is not None:
86
+ return f"{name} ({_fmt_size(size)})"
87
+ return name
88
+
89
+ fmt = format_entry or default_format
90
+
91
+ # Collect entries: subdirs first, then files
92
+ entries = []
93
+ subdirs = sorted(k for k in node.keys() if k not in ("__files__", "__meta__"))
94
+ files_here = sorted(node.get("__files__", []), key=lambda x: x[0])
95
+
96
+ for dirname in subdirs:
97
+ dir_meta = node[dirname].get("__meta__", {})
98
+ entries.append(("dir", dirname, node[dirname], dir_meta))
99
+ for fname, fmeta in files_here:
100
+ entries.append(("file", fname, None, fmeta))
101
+
102
+ for i, entry in enumerate(entries):
103
+ is_last = (i == len(entries) - 1)
104
+ connector = "└── " if is_last else "├── "
105
+ child_prefix = prefix + (" " if is_last else "│ ")
106
+
107
+ etype, name, subtree, meta = entry
108
+
109
+ if etype == "dir":
110
+ result.append(f"{prefix}{connector}{fmt(name, meta, True)}")
111
+ result.extend(render_tree(subtree, child_prefix, format_entry))
112
+ else:
113
+ result.append(f"{prefix}{connector}{fmt(name, meta, False)}")
114
+
115
+ return result
116
+
117
+
118
+ def _fmt_size(num_bytes: int) -> str:
119
+ """Format byte size as human-readable string."""
120
+ units = ["B", "KB", "MB", "GB"]
121
+ size = float(num_bytes)
122
+ for unit in units:
123
+ if size < 1024.0:
124
+ return f"{size:.1f} {unit}"
125
+ size /= 1024.0
126
+ return f"{size:.1f} TB"
127
+
128
+
129
+ def walk_and_build_tree(
130
+ abs_path: str,
131
+ *,
132
+ show_hidden: bool = False,
133
+ recursive: bool = False,
134
+ max_entries: int = 100,
135
+ ) -> tuple[dict, int, bool]:
136
+ """
137
+ Walk a directory and build a tree structure.
138
+
139
+ Args:
140
+ abs_path: Absolute path to directory
141
+ show_hidden: Include hidden files/dirs (starting with '.')
142
+ recursive: Recurse into subdirectories
143
+ max_entries: Maximum entries before truncation
144
+
145
+ Returns:
146
+ (tree, total_entries, truncated)
147
+ """
148
+ entries: list[tuple[str, dict]] = []
149
+ total = 0
150
+ truncated = False
151
+
152
+ for root, dirs, files in os.walk(abs_path):
153
+ # Filter hidden
154
+ if not show_hidden:
155
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
156
+ files = [f for f in files if not f.startswith('.')]
157
+
158
+ dirs.sort()
159
+ files.sort()
160
+
161
+ # Compute relative path from the listing root
162
+ try:
163
+ rel_root = os.path.relpath(root, abs_path)
164
+ except Exception:
165
+ rel_root = ""
166
+ prefix = "" if rel_root == "." else rel_root.replace("\\", "/") + "/"
167
+
168
+ # Add directories (with trailing slash to indicate dir)
169
+ for d in dirs:
170
+ p = os.path.join(root, d)
171
+ try:
172
+ mtime = datetime.fromtimestamp(os.path.getmtime(p)).strftime("%Y-%m-%d %H:%M")
173
+ except Exception:
174
+ mtime = "?"
175
+ entries.append((f"{prefix}{d}/", {"mtime": mtime}))
176
+ total += 1
177
+ if total >= max_entries:
178
+ truncated = True
179
+ break
180
+
181
+ if truncated:
182
+ break
183
+
184
+ # Add files
185
+ for f in files:
186
+ p = os.path.join(root, f)
187
+ try:
188
+ size = os.path.getsize(p)
189
+ mtime = datetime.fromtimestamp(os.path.getmtime(p)).strftime("%Y-%m-%d %H:%M")
190
+ except Exception:
191
+ size, mtime = 0, "?"
192
+ entries.append((f"{prefix}{f}", {"size": size, "mtime": mtime}))
193
+ total += 1
194
+ if total >= max_entries:
195
+ truncated = True
196
+ break
197
+
198
+ if truncated:
199
+ break
200
+
201
+ if not recursive:
202
+ break
203
+
204
+ return build_tree(entries), total, truncated
205
+
206
+
207
+ def format_dir_listing(
208
+ abs_path: str,
209
+ display_path: str,
210
+ *,
211
+ show_hidden: bool = False,
212
+ recursive: bool = False,
213
+ max_entries: int = 100,
214
+ fmt_size_fn: Optional[Callable[[int], str]] = None,
215
+ ) -> str:
216
+ """
217
+ Format a directory listing as a visual tree.
218
+
219
+ Args:
220
+ abs_path: Absolute path to directory
221
+ display_path: User-friendly path to show in header
222
+ show_hidden: Include hidden files/dirs
223
+ recursive: Recurse into subdirectories
224
+ max_entries: Maximum entries before truncation
225
+ fmt_size_fn: Optional custom size formatter (defaults to _fmt_size)
226
+
227
+ Returns:
228
+ Formatted string with tree output.
229
+ """
230
+ fmt_size = fmt_size_fn or _fmt_size
231
+
232
+ tree, total, truncated = walk_and_build_tree(
233
+ abs_path,
234
+ show_hidden=show_hidden,
235
+ recursive=recursive,
236
+ max_entries=max_entries,
237
+ )
238
+
239
+ # Formatter with size + date
240
+ def format_entry(name: str, meta: dict, is_dir: bool) -> str:
241
+ mtime = meta.get("mtime", "")
242
+ if is_dir:
243
+ return f"{name}/ ({mtime})"
244
+ size = meta.get("size", 0)
245
+ return f"{name} ({fmt_size(size)}, {mtime})"
246
+
247
+ tree_lines = render_tree(tree, " ", format_entry)
248
+
249
+ header = f"Listing of {display_path}\nRoot: /\nEntries: {total}"
250
+ if truncated:
251
+ header += f"\n… Truncated at {max_entries} entries."
252
+
253
+ lines = [header, "", "└── /"]
254
+ lines.extend(tree_lines)
255
+
256
+ return "\n".join(lines).strip()
257
+
258
+
259
+ __all__ = [
260
+ "build_tree",
261
+ "render_tree",
262
+ "_fmt_size",
263
+ "walk_and_build_tree",
264
+ "format_dir_listing",
265
+ ]