python-du 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
python_du/renderer.py CHANGED
@@ -54,12 +54,13 @@ def render_tree(root: DirNode, config: RenderConfig | None = None) -> str:
54
54
  lines: list[str] = []
55
55
 
56
56
  total = root.total_size or 1
57
+ label_col = (cfg.max_depth + 1) * 4 + 24
57
58
 
58
59
  header = f"{BOLD}{root.name}{RESET}" if cfg.use_color else root.name
59
60
  lines.append(f"{header} [{format_size(root.total_size)}]")
60
61
  lines.append("")
61
62
 
62
- _render_node(root, lines, cfg, bar_width, total, prefix="", depth=0, color_idx=0)
63
+ _render_node(root, lines, cfg, bar_width, total, prefix="", depth=0, color_idx=0, label_col=label_col)
63
64
 
64
65
  lines.append("")
65
66
  lines.append(_summary_line(root, cfg))
@@ -75,24 +76,25 @@ def _render_node(
75
76
  prefix: str,
76
77
  depth: int,
77
78
  color_idx: int,
79
+ label_col: int,
78
80
  ) -> None:
79
81
  children = node.children
80
82
  if cfg.sort_by_size:
81
83
  children = sorted(children, key=lambda c: c.total_size, reverse=True)
82
84
 
83
85
  own_files_size = node.own_size
84
- entries: list[tuple[str, int, DirNode | None]] = []
86
+ entries: list[tuple[str, int, int, DirNode | None]] = []
85
87
  for child in children:
86
88
  pct = (child.total_size / root_total * 100) if root_total else 0
87
89
  if pct >= cfg.min_percent or depth < 1:
88
- entries.append((child.name, child.total_size, child))
90
+ entries.append((child.name, child.total_size, _count_files(child), child))
89
91
 
90
92
  if own_files_size > 0:
91
93
  pct = (own_files_size / root_total * 100) if root_total else 0
92
94
  if pct >= cfg.min_percent or depth < 1:
93
- entries.append(("<files>", own_files_size, None))
95
+ entries.append(("<files>", own_files_size, node.file_count, None))
94
96
 
95
- for i, (name, size, child_node) in enumerate(entries):
97
+ for i, (name, size, fcount, child_node) in enumerate(entries):
96
98
  is_last = i == len(entries) - 1
97
99
  connector = TREE_BEND if is_last else TREE_TEE
98
100
  child_prefix = TREE_BLANK if is_last else TREE_PIPE
@@ -102,15 +104,16 @@ def _render_node(
102
104
 
103
105
  size_str = format_size(size)
104
106
  pct_str = f" ({pct:5.1f}%)" if cfg.show_percent else ""
107
+ files_str = f" [{fcount:,} {'file' if fcount == 1 else 'files'}]" if fcount else ""
105
108
 
106
109
  if cfg.use_color and child_node is None:
107
110
  name_str = f"{DIM}{name}{RESET}"
108
- elif cfg.use_color:
109
- name_str = name
110
111
  else:
111
112
  name_str = name
112
113
 
113
- lines.append(f"{prefix}{connector}{name_str:<30s} {size_str:>10s}{pct_str} {bar}")
114
+ visible_len = len(prefix) + len(connector) + len(name)
115
+ padding = max(1, label_col - visible_len)
116
+ lines.append(f"{prefix}{connector}{name_str}{' ' * padding}{size_str:>10s}{pct_str} {bar}{files_str}")
114
117
 
115
118
  if child_node and depth < cfg.max_depth:
116
119
  _render_node(
@@ -122,6 +125,7 @@ def _render_node(
122
125
  prefix=prefix + child_prefix,
123
126
  depth=depth + 1,
124
127
  color_idx=color_idx + i,
128
+ label_col=label_col,
125
129
  )
126
130
 
127
131
 
python_du/scanner.py CHANGED
@@ -21,6 +21,11 @@ class DirNode:
21
21
  return self.path.name or str(self.path)
22
22
 
23
23
 
24
+ def _allocated_size(stat_result: os.stat_result) -> int:
25
+ blocks = getattr(stat_result, "st_blocks", None)
26
+ return stat_result.st_size if blocks is None else blocks * 512
27
+
28
+
24
29
  def scan(root: Path, *, max_depth: int | None = None, follow_symlinks: bool = False) -> DirNode:
25
30
  """Walk *root* and return a tree of DirNode objects with computed sizes."""
26
31
  return _scan_dir(root.resolve(), depth=0, max_depth=max_depth, follow_symlinks=follow_symlinks)
@@ -51,7 +56,8 @@ def _scan_dir(
51
56
 
52
57
  if entry.is_file(follow_symlinks=follow_symlinks):
53
58
  try:
54
- node.own_size += entry.stat(follow_symlinks=follow_symlinks).st_size
59
+ stat_result = entry.stat(follow_symlinks=follow_symlinks)
60
+ node.own_size += _allocated_size(stat_result)
55
61
  node.file_count += 1
56
62
  except OSError:
57
63
  pass
@@ -83,7 +89,8 @@ def _shallow_size(path: Path) -> DirNode:
83
89
  for dirpath, _dirnames, filenames in os.walk(path):
84
90
  for fname in filenames:
85
91
  try:
86
- total += os.path.getsize(os.path.join(dirpath, fname))
92
+ stat_result = os.stat(os.path.join(dirpath, fname))
93
+ total += _allocated_size(stat_result)
87
94
  fcount += 1
88
95
  except OSError:
89
96
  pass
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-du
3
+ Version: 0.1.2
4
+ Summary: A disk usage analyzer that prints a visual map of folder sizes
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+
8
+ # python-du
9
+
10
+ A disk usage analyzer that scans a directory and prints a visual tree map of folder sizes — right in your terminal.
11
+
12
+ ## Example
13
+
14
+ ![](https://github.com/quantrpeter/python-du/blob/main/image/Screenshot%20from%202026-03-06%2011-31-47.png?raw=true)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install -e .
20
+ ```
21
+
22
+ ```
23
+ pip uninstall python-du
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ python-du [path] [options]
30
+ ```
31
+
32
+ If no path is given, the current directory is scanned.
33
+
34
+ ### Options
35
+
36
+ | Flag | Description |
37
+ |------|-------------|
38
+ | `-d`, `--max-depth N` | Maximum tree depth to display (default: 4) |
39
+ | `-m`, `--min-percent N` | Hide entries smaller than N% of total (default: 0.5) |
40
+ | `--no-color` | Disable colored output |
41
+ | `--sort-alpha` | Sort alphabetically instead of by size |
42
+ | `--scan-depth N` | Limit how deep the filesystem scan goes (default: unlimited) |
43
+ | `-L`, `--follow-symlinks` | Follow symbolic links |
44
+
45
+ ### Examples
46
+
47
+ Scan the current directory with defaults:
48
+
49
+ ```bash
50
+ python-du
51
+ ```
52
+
53
+ Scan `/var/log` showing only entries above 5%, two levels deep:
54
+
55
+ ```bash
56
+ python-du /var/log -d 2 -m 5
57
+ ```
58
+
59
+ Pipe to a file (automatically disables color):
60
+
61
+ ```bash
62
+ python-du /home > usage-report.txt
63
+ ```
64
+
65
+ ## Publishing to PyPI
66
+
67
+ ```
68
+ pip install build twine
69
+ python -m build
70
+ twine upload dist/*
71
+ ```
72
+
73
+ When prompted, use `__token__` as the username and your PyPI API token as the password.
74
+
75
+ To skip the prompt, create a `~/.pypirc` file:
76
+
77
+ ```ini
78
+ [pypi]
79
+ username = __token__
80
+ password = pypi-YOUR-TOKEN-HERE
81
+ ```
82
+
83
+ > **Note:** Bump the `version` in `pyproject.toml` before each upload — PyPI rejects duplicate version numbers.
84
+
85
+ ## Requirements
86
+
87
+ - Python >= 3.9
88
+ - No external dependencies
@@ -0,0 +1,9 @@
1
+ python_du/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ python_du/cli.py,sha256=0dY0IorPKHsqG7mPhF9ElXEqnZeX868HmTij6v7APZ0,2144
3
+ python_du/renderer.py,sha256=ucx0CtkilXv42BgsA30ZI8qKt_D24DFM2IEYyJxmND0,4932
4
+ python_du/scanner.py,sha256=kIDjFybIwaEmwlJSR2mCWlaG6-cLufZPtM-Rfz-ay1M,3171
5
+ python_du-0.1.2.dist-info/METADATA,sha256=GZXkv7zfG3I_1t-ZEOa8lsFCYj0vFGASgrl_FlWaX0Q,1850
6
+ python_du-0.1.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ python_du-0.1.2.dist-info/entry_points.txt,sha256=m8Km0ZaanWCYWdBPcLTC3XREnJtG-CwE95yKdAli9kw,49
8
+ python_du-0.1.2.dist-info/top_level.txt,sha256=PLwVAljOX1Zv_ACc2YSYHmL7MhJ_szkcvz7XyY2StKc,10
9
+ python_du-0.1.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,5 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-du
3
- Version: 0.1.0
4
- Summary: A disk usage analyzer that prints a visual map of folder sizes
5
- Requires-Python: >=3.9
@@ -1,9 +0,0 @@
1
- python_du/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- python_du/cli.py,sha256=0dY0IorPKHsqG7mPhF9ElXEqnZeX868HmTij6v7APZ0,2144
3
- python_du/renderer.py,sha256=grBwc1TCVRaTRG2NFU4I-9yyIYasV7Q6GZDhoKL5HQ0,4589
4
- python_du/scanner.py,sha256=nmfI8bL9_OzOST44_pN-BlZ3XkNWBQ5aDRNm4DMaPS8,2882
5
- python_du-0.1.0.dist-info/METADATA,sha256=d06m7hakzgKnzo3TyLgw2dy0eWe6R3CF5ZglaH-D8sM,148
6
- python_du-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
7
- python_du-0.1.0.dist-info/entry_points.txt,sha256=m8Km0ZaanWCYWdBPcLTC3XREnJtG-CwE95yKdAli9kw,49
8
- python_du-0.1.0.dist-info/top_level.txt,sha256=PLwVAljOX1Zv_ACc2YSYHmL7MhJ_szkcvz7XyY2StKc,10
9
- python_du-0.1.0.dist-info/RECORD,,