treex-cli 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- {treex_cli-0.2.2 → treex_cli-0.2.4}/Cargo.lock +17 -17
- {treex_cli-0.2.2 → treex_cli-0.2.4}/Cargo.toml +1 -1
- {treex_cli-0.2.2 → treex_cli-0.2.4}/PKG-INFO +3 -3
- {treex_cli-0.2.2 → treex_cli-0.2.4}/README.md +2 -2
- {treex_cli-0.2.2 → treex_cli-0.2.4}/pyproject.toml +7 -8
- {treex_cli-0.2.2 → treex_cli-0.2.4}/src/treex/treex.py +91 -41
- {treex_cli-0.2.2 → treex_cli-0.2.4}/src/treex_cli.egg-info/PKG-INFO +3 -3
- {treex_cli-0.2.2 → treex_cli-0.2.4}/tests/test_treex.py +28 -12
- {treex_cli-0.2.2 → treex_cli-0.2.4}/LICENSE +0 -0
- {treex_cli-0.2.2 → treex_cli-0.2.4}/MANIFEST.in +0 -0
- {treex_cli-0.2.2 → treex_cli-0.2.4}/rust/lib.rs +0 -0
- {treex_cli-0.2.2 → treex_cli-0.2.4}/setup.cfg +0 -0
- {treex_cli-0.2.2 → treex_cli-0.2.4}/src/treex/__init__.py +0 -0
- {treex_cli-0.2.2 → treex_cli-0.2.4}/src/treex_cli.egg-info/SOURCES.txt +0 -0
- {treex_cli-0.2.2 → treex_cli-0.2.4}/src/treex_cli.egg-info/dependency_links.txt +0 -0
- {treex_cli-0.2.2 → treex_cli-0.2.4}/src/treex_cli.egg-info/entry_points.txt +0 -0
- {treex_cli-0.2.2 → treex_cli-0.2.4}/src/treex_cli.egg-info/top_level.txt +0 -0
|
@@ -352,9 +352,9 @@ dependencies = [
|
|
|
352
352
|
|
|
353
353
|
[[package]]
|
|
354
354
|
name = "gix-config"
|
|
355
|
-
version = "0.62.
|
|
355
|
+
version = "0.62.2"
|
|
356
356
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
357
|
-
checksum = "
|
|
357
|
+
checksum = "3e11ccf82e9630c4e7befbae07cbe90e79de61dc73a34448c2dec70a13da9ebf"
|
|
358
358
|
dependencies = [
|
|
359
359
|
"bstr",
|
|
360
360
|
"gix-config-value",
|
|
@@ -496,9 +496,9 @@ dependencies = [
|
|
|
496
496
|
|
|
497
497
|
[[package]]
|
|
498
498
|
name = "gix-hashtable"
|
|
499
|
-
version = "0.18.
|
|
499
|
+
version = "0.18.1"
|
|
500
500
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
501
|
-
checksum = "
|
|
501
|
+
checksum = "2f66822aee11ccb28d6a2e63b587254e578fb52a138bd5806b58d8560125592b"
|
|
502
502
|
dependencies = [
|
|
503
503
|
"gix-hash",
|
|
504
504
|
"hashbrown 0.17.1",
|
|
@@ -507,9 +507,9 @@ dependencies = [
|
|
|
507
507
|
|
|
508
508
|
[[package]]
|
|
509
509
|
name = "gix-ignore"
|
|
510
|
-
version = "0.24.
|
|
510
|
+
version = "0.24.1"
|
|
511
511
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
512
|
-
checksum = "
|
|
512
|
+
checksum = "968f12fe42896873504f46127cd5bf0f9a34fc1479a781f43cf94b5d8d760a15"
|
|
513
513
|
dependencies = [
|
|
514
514
|
"bstr",
|
|
515
515
|
"gix-glob",
|
|
@@ -660,9 +660,9 @@ dependencies = [
|
|
|
660
660
|
|
|
661
661
|
[[package]]
|
|
662
662
|
name = "gix-parallel"
|
|
663
|
-
version = "0.50.
|
|
663
|
+
version = "0.50.1"
|
|
664
664
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
665
|
-
checksum = "
|
|
665
|
+
checksum = "58e7e10f8343e05f6ad8651f97dffa1caca1531bb274bfc8e3050e80e3ed5946"
|
|
666
666
|
dependencies = [
|
|
667
667
|
"crossbeam-channel",
|
|
668
668
|
"gix-trace",
|
|
@@ -777,9 +777,9 @@ dependencies = [
|
|
|
777
777
|
|
|
778
778
|
[[package]]
|
|
779
779
|
name = "gix-sec"
|
|
780
|
-
version = "0.16.
|
|
780
|
+
version = "0.16.1"
|
|
781
781
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
782
|
-
checksum = "
|
|
782
|
+
checksum = "9598d1f35baec7038c2d278bfcc18f1d8951f41dcc3f51ab6175661dfe5a6137"
|
|
783
783
|
dependencies = [
|
|
784
784
|
"bitflags 2.13.2",
|
|
785
785
|
"gix-path",
|
|
@@ -869,9 +869,9 @@ dependencies = [
|
|
|
869
869
|
|
|
870
870
|
[[package]]
|
|
871
871
|
name = "gix-utils"
|
|
872
|
-
version = "0.5.
|
|
872
|
+
version = "0.5.2"
|
|
873
873
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
874
|
-
checksum = "
|
|
874
|
+
checksum = "11a9446060447e9ec58f5d333b98bed53389dfa10de5d8dd46fc76d01dfc2954"
|
|
875
875
|
dependencies = [
|
|
876
876
|
"bstr",
|
|
877
877
|
"bytes",
|
|
@@ -1257,9 +1257,9 @@ checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
|
|
1257
1257
|
|
|
1258
1258
|
[[package]]
|
|
1259
1259
|
name = "rustix"
|
|
1260
|
-
version = "1.1.
|
|
1260
|
+
version = "1.1.6"
|
|
1261
1261
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1262
|
-
checksum = "
|
|
1262
|
+
checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa"
|
|
1263
1263
|
dependencies = [
|
|
1264
1264
|
"bitflags 2.13.2",
|
|
1265
1265
|
"errno",
|
|
@@ -1332,9 +1332,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
|
|
1332
1332
|
|
|
1333
1333
|
[[package]]
|
|
1334
1334
|
name = "smallvec"
|
|
1335
|
-
version = "1.16.
|
|
1335
|
+
version = "1.16.4"
|
|
1336
1336
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1337
|
-
checksum = "
|
|
1337
|
+
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
|
|
1338
1338
|
|
|
1339
1339
|
[[package]]
|
|
1340
1340
|
name = "stable_deref_trait"
|
|
@@ -1411,7 +1411,7 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
|
|
1411
1411
|
|
|
1412
1412
|
[[package]]
|
|
1413
1413
|
name = "treex-native"
|
|
1414
|
-
version = "0.2.
|
|
1414
|
+
version = "0.2.4"
|
|
1415
1415
|
dependencies = [
|
|
1416
1416
|
"gix",
|
|
1417
1417
|
"pyo3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: treex-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Command-line directory tree utility with metadata and Git ignore support
|
|
5
5
|
Author: Corey Goldberg
|
|
6
6
|
Maintainer: Corey Goldberg
|
|
@@ -110,8 +110,8 @@ Prebuilt wheels are available for common platforms. See
|
|
|
110
110
|
## Git ignore support
|
|
111
111
|
|
|
112
112
|
`treex` uses [Gitoxide][gitoxide-home] (`gix`), a native Rust implementation of
|
|
113
|
-
Git
|
|
114
|
-
|
|
113
|
+
Git, to evaluate `.gitignore` rules without invoking the Git executable. Git
|
|
114
|
+
does not need to be installed for ignore matching to work.
|
|
115
115
|
|
|
116
116
|
When the specified directory is inside a Git working tree, `treex` respects
|
|
117
117
|
applicable `.gitignore` rules by default. Outside a Git working tree, Git ignore
|
|
@@ -87,8 +87,8 @@ Prebuilt wheels are available for common platforms. See
|
|
|
87
87
|
## Git ignore support
|
|
88
88
|
|
|
89
89
|
`treex` uses [Gitoxide][gitoxide-home] (`gix`), a native Rust implementation of
|
|
90
|
-
Git
|
|
91
|
-
|
|
90
|
+
Git, to evaluate `.gitignore` rules without invoking the Git executable. Git
|
|
91
|
+
does not need to be installed for ignore matching to work.
|
|
92
92
|
|
|
93
93
|
When the specified directory is inside a Git working tree, `treex` respects
|
|
94
94
|
applicable `.gitignore` rules by default. Outside a Git working tree, Git ignore
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "treex-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description="Command-line directory tree utility with metadata and Git ignore support"
|
|
9
9
|
requires-python = ">= 3.13"
|
|
10
10
|
license = "MIT"
|
|
@@ -37,13 +37,14 @@ repository = "https://github.com/cgoldberg/treex"
|
|
|
37
37
|
|
|
38
38
|
[dependency-groups]
|
|
39
39
|
build = [
|
|
40
|
-
"cibuildwheel==4.3.
|
|
40
|
+
"cibuildwheel==4.3.5",
|
|
41
|
+
"twine==7.0.0",
|
|
41
42
|
]
|
|
42
43
|
lint = [
|
|
43
|
-
"ruff==0.17.
|
|
44
|
+
"ruff==0.17.3",
|
|
44
45
|
]
|
|
45
46
|
test = [
|
|
46
|
-
"pytest==9.
|
|
47
|
+
"pytest==9.2.0",
|
|
47
48
|
]
|
|
48
49
|
validate = [
|
|
49
50
|
"validate-pyproject[all]==0.27",
|
|
@@ -64,12 +65,10 @@ py-limited-api = true
|
|
|
64
65
|
|
|
65
66
|
[tool.cibuildwheel]
|
|
66
67
|
build = "cp313-*"
|
|
67
|
-
test-requires = ["pytest==9.1.1"]
|
|
68
|
-
test-command = "pytest {project}/tests"
|
|
69
|
-
|
|
70
|
-
[tool.cibuildwheel.linux]
|
|
71
68
|
before-all = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal"
|
|
72
69
|
environment = { PATH = "$HOME/.cargo/bin:$PATH" }
|
|
70
|
+
test-requires = ["pytest==9.2.0"]
|
|
71
|
+
test-command = "pytest {project}"
|
|
73
72
|
|
|
74
73
|
[tool.pytest]
|
|
75
74
|
addopts = ["-v"]
|
|
@@ -15,6 +15,7 @@ the filesystem is scanned normally without Git filtering.
|
|
|
15
15
|
|
|
16
16
|
import argparse
|
|
17
17
|
import fnmatch
|
|
18
|
+
import os
|
|
18
19
|
import re
|
|
19
20
|
import sys
|
|
20
21
|
from datetime import datetime
|
|
@@ -29,6 +30,9 @@ except PackageNotFoundError:
|
|
|
29
30
|
__version__ = "unknown"
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
PROG = "treex"
|
|
34
|
+
|
|
35
|
+
|
|
32
36
|
class GitIgnore:
|
|
33
37
|
"""Apply Git ignore rules using Gitoxide when inside a repository."""
|
|
34
38
|
|
|
@@ -53,26 +57,29 @@ class GitIgnore:
|
|
|
53
57
|
"""Check if Git considers a path ignored."""
|
|
54
58
|
return bool(self.ignored_many([path]))
|
|
55
59
|
|
|
56
|
-
def ignored_many(self, paths):
|
|
57
|
-
"""
|
|
60
|
+
def ignored_many(self, paths, directory_flags=None):
|
|
61
|
+
"""Return the paths that Git considers ignored."""
|
|
58
62
|
if not self.enabled or self.repo_root is None or self.matcher is None:
|
|
59
63
|
return set()
|
|
60
|
-
|
|
64
|
+
candidates = []
|
|
61
65
|
matcher_paths = []
|
|
62
66
|
for path in paths:
|
|
63
67
|
try:
|
|
64
|
-
relative = path.
|
|
68
|
+
relative = path.relative_to(self.repo_root)
|
|
65
69
|
except (ValueError, OSError):
|
|
66
70
|
continue
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
candidates.append(path)
|
|
72
|
+
is_dir = (
|
|
73
|
+
directory_flags[path]
|
|
74
|
+
if directory_flags is not None and path in directory_flags
|
|
75
|
+
else path.is_dir()
|
|
76
|
+
)
|
|
77
|
+
matcher_paths.append((relative.as_posix(), is_dir))
|
|
69
78
|
if not matcher_paths:
|
|
70
79
|
return set()
|
|
71
80
|
results = self.matcher.ignored_many(matcher_paths)
|
|
72
81
|
return {
|
|
73
|
-
|
|
74
|
-
for relative, ignored in zip(relative_paths, results, strict=True)
|
|
75
|
-
if ignored
|
|
82
|
+
path for path, ignored in zip(candidates, results, strict=True) if ignored
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
|
|
@@ -125,7 +132,14 @@ def _count_lines(path):
|
|
|
125
132
|
|
|
126
133
|
|
|
127
134
|
def _file_metadata(path):
|
|
128
|
-
"""
|
|
135
|
+
"""Collect file metadata, identifying binary files and counting text lines.
|
|
136
|
+
|
|
137
|
+
Reads files in chunks to avoid loading large files into memory. Checks
|
|
138
|
+
the initial bytes for NUL characters and invalid UTF-8 to identify binary
|
|
139
|
+
files, and counts lines for text files in a single pass. Empty files are
|
|
140
|
+
handled without opening them. Returns formatted size, line count or file
|
|
141
|
+
status, byte size, and modification time.
|
|
142
|
+
"""
|
|
129
143
|
try:
|
|
130
144
|
stat = path.stat()
|
|
131
145
|
size = stat.st_size
|
|
@@ -133,37 +147,71 @@ def _file_metadata(path):
|
|
|
133
147
|
except OSError:
|
|
134
148
|
return "?", "[unreadable]", 0, None
|
|
135
149
|
size_text = _format_size(size)
|
|
136
|
-
if
|
|
150
|
+
if size == 0:
|
|
151
|
+
return size_text, "0 lines", size, modified
|
|
152
|
+
try:
|
|
153
|
+
f = path.open("rb")
|
|
154
|
+
except OSError:
|
|
137
155
|
return size_text, "[binary]", size, modified
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
with f:
|
|
157
|
+
try:
|
|
158
|
+
chunk = f.read(8192)
|
|
159
|
+
except OSError:
|
|
160
|
+
return size_text, "[binary]", size, modified
|
|
161
|
+
if chunk:
|
|
162
|
+
if b"\x00" in chunk:
|
|
163
|
+
return size_text, "[binary]", size, modified
|
|
164
|
+
try:
|
|
165
|
+
chunk.decode("utf-8")
|
|
166
|
+
except UnicodeDecodeError:
|
|
167
|
+
return size_text, "[binary]", size, modified
|
|
168
|
+
lines = chunk.count(b"\n")
|
|
169
|
+
last_byte = chunk[-1:] if chunk else b""
|
|
170
|
+
has_data = bool(chunk)
|
|
171
|
+
# A short read from a regular file normally means EOF.
|
|
172
|
+
if len(chunk) == 8192:
|
|
173
|
+
try:
|
|
174
|
+
while True:
|
|
175
|
+
chunk = f.read(65536)
|
|
176
|
+
if not chunk:
|
|
177
|
+
break
|
|
178
|
+
has_data = True
|
|
179
|
+
lines += chunk.count(b"\n")
|
|
180
|
+
last_byte = chunk[-1:]
|
|
181
|
+
except OSError:
|
|
182
|
+
return size_text, "[unreadable]", size, modified
|
|
183
|
+
if has_data and last_byte != b"\n":
|
|
184
|
+
lines += 1
|
|
141
185
|
return size_text, f"{lines:,} lines", size, modified
|
|
142
186
|
|
|
143
187
|
|
|
144
188
|
def _get_tree_entries(path, gitignore):
|
|
145
|
-
"""Get entries to include in the tree."""
|
|
189
|
+
"""Get entries to include in the tree with cached type information."""
|
|
190
|
+
typed_entries = []
|
|
146
191
|
try:
|
|
147
|
-
|
|
192
|
+
# Use os.scandir() instead of Path.iterdir(): DirEntry caches
|
|
193
|
+
# filesystem metadata, reducing extra system calls for type checks.
|
|
194
|
+
entries = os.scandir(path)
|
|
148
195
|
except PermissionError:
|
|
149
196
|
return None
|
|
150
|
-
|
|
197
|
+
with entries:
|
|
198
|
+
for dir_entry in entries:
|
|
199
|
+
if dir_entry.is_symlink():
|
|
200
|
+
continue
|
|
201
|
+
is_dir = dir_entry.is_dir(follow_symlinks=False)
|
|
202
|
+
is_file = dir_entry.is_file(follow_symlinks=False)
|
|
203
|
+
typed_entries.append((Path(dir_entry.path), is_dir, is_file))
|
|
151
204
|
if gitignore is not None:
|
|
152
|
-
|
|
205
|
+
typed_entries = [item for item in typed_entries if item[0].name != ".git"]
|
|
153
206
|
if gitignore.enabled and gitignore.repo_root is not None:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if relative.as_posix() not in ignored:
|
|
163
|
-
filtered_entries.append(entry)
|
|
164
|
-
entries = filtered_entries
|
|
165
|
-
entries.sort(key=lambda p: (p.is_file(), p.name.casefold()))
|
|
166
|
-
return entries
|
|
207
|
+
directory_flags = {entry: is_dir for entry, is_dir, _ in typed_entries}
|
|
208
|
+
ignored = gitignore.ignored_many(
|
|
209
|
+
[entry for entry, _, _ in typed_entries],
|
|
210
|
+
directory_flags,
|
|
211
|
+
)
|
|
212
|
+
typed_entries = [item for item in typed_entries if item[0] not in ignored]
|
|
213
|
+
typed_entries.sort(key=lambda item: (item[2], item[0].name.casefold()))
|
|
214
|
+
return typed_entries
|
|
167
215
|
|
|
168
216
|
|
|
169
217
|
def _matches_pattern(path, pattern):
|
|
@@ -183,6 +231,7 @@ def _scan_tree(
|
|
|
183
231
|
max_depth=None,
|
|
184
232
|
depth=0,
|
|
185
233
|
root=None,
|
|
234
|
+
relative_prefix="",
|
|
186
235
|
):
|
|
187
236
|
"""Scan and filter the tree, returning nodes for rendering.
|
|
188
237
|
|
|
@@ -194,8 +243,8 @@ def _scan_tree(
|
|
|
194
243
|
Directory sizes include only files within the permitted depth
|
|
195
244
|
that pass the include/exclude filters.
|
|
196
245
|
"""
|
|
197
|
-
path = path.resolve()
|
|
198
246
|
if root is None:
|
|
247
|
+
path = path.resolve()
|
|
199
248
|
root = path
|
|
200
249
|
if max_depth is not None and depth >= max_depth:
|
|
201
250
|
return []
|
|
@@ -214,14 +263,14 @@ def _scan_tree(
|
|
|
214
263
|
]
|
|
215
264
|
nodes = []
|
|
216
265
|
has_include = include_pattern is not None or include_regex is not None
|
|
217
|
-
for entry in entries:
|
|
218
|
-
|
|
219
|
-
|
|
266
|
+
for entry, is_dir, is_file in entries:
|
|
267
|
+
name = entry.name
|
|
268
|
+
relative_path = f"{relative_prefix}/{name}" if relative_prefix else name
|
|
220
269
|
if _matches_pattern(relative_path, exclude_pattern):
|
|
221
270
|
continue
|
|
222
271
|
if exclude_regex is not None and exclude_regex.search(relative_path):
|
|
223
272
|
continue
|
|
224
|
-
if
|
|
273
|
+
if is_dir:
|
|
225
274
|
entry_depth = depth + 1
|
|
226
275
|
if max_depth is not None and entry_depth >= max_depth:
|
|
227
276
|
children = []
|
|
@@ -238,6 +287,7 @@ def _scan_tree(
|
|
|
238
287
|
max_depth,
|
|
239
288
|
entry_depth,
|
|
240
289
|
root,
|
|
290
|
+
relative_path,
|
|
241
291
|
)
|
|
242
292
|
contains_match = any(child["contains_match"] for child in children)
|
|
243
293
|
if has_include and not contains_match:
|
|
@@ -247,7 +297,7 @@ def _scan_tree(
|
|
|
247
297
|
)
|
|
248
298
|
nodes.append(
|
|
249
299
|
{
|
|
250
|
-
"name":
|
|
300
|
+
"name": name,
|
|
251
301
|
"kind": "directory",
|
|
252
302
|
"children": children,
|
|
253
303
|
"size": size,
|
|
@@ -257,7 +307,7 @@ def _scan_tree(
|
|
|
257
307
|
}
|
|
258
308
|
)
|
|
259
309
|
continue
|
|
260
|
-
if not
|
|
310
|
+
if not is_file:
|
|
261
311
|
continue
|
|
262
312
|
if include_pattern is not None and not _matches_pattern(
|
|
263
313
|
relative_path,
|
|
@@ -272,7 +322,7 @@ def _scan_tree(
|
|
|
272
322
|
size_text, info, size, modified = None, None, None, None
|
|
273
323
|
nodes.append(
|
|
274
324
|
{
|
|
275
|
-
"name":
|
|
325
|
+
"name": name,
|
|
276
326
|
"kind": "file",
|
|
277
327
|
"children": [],
|
|
278
328
|
"size": size,
|
|
@@ -455,7 +505,7 @@ def parse_args(argv=None):
|
|
|
455
505
|
parser = argparse.ArgumentParser(
|
|
456
506
|
description="List directory contents as a tree "
|
|
457
507
|
"with file and directory metadata.",
|
|
458
|
-
usage="
|
|
508
|
+
usage=f"{PROG} [OPTIONS] [directory]",
|
|
459
509
|
formatter_class=formatter,
|
|
460
510
|
)
|
|
461
511
|
parser.add_argument(
|
|
@@ -469,7 +519,7 @@ def parse_args(argv=None):
|
|
|
469
519
|
"-V",
|
|
470
520
|
"--version",
|
|
471
521
|
action="version",
|
|
472
|
-
version=f"
|
|
522
|
+
version=f"{PROG} {__version__}",
|
|
473
523
|
help="show version information and exit",
|
|
474
524
|
)
|
|
475
525
|
parser.add_argument(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: treex-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Command-line directory tree utility with metadata and Git ignore support
|
|
5
5
|
Author: Corey Goldberg
|
|
6
6
|
Maintainer: Corey Goldberg
|
|
@@ -110,8 +110,8 @@ Prebuilt wheels are available for common platforms. See
|
|
|
110
110
|
## Git ignore support
|
|
111
111
|
|
|
112
112
|
`treex` uses [Gitoxide][gitoxide-home] (`gix`), a native Rust implementation of
|
|
113
|
-
Git
|
|
114
|
-
|
|
113
|
+
Git, to evaluate `.gitignore` rules without invoking the Git executable. Git
|
|
114
|
+
does not need to be installed for ignore matching to work.
|
|
115
115
|
|
|
116
116
|
When the specified directory is inside a Git working tree, `treex` respects
|
|
117
117
|
applicable `.gitignore` rules by default. Outside a Git working tree, Git ignore
|
|
@@ -223,7 +223,7 @@ class TestGitIgnore:
|
|
|
223
223
|
ignored_dir.mkdir()
|
|
224
224
|
paths = [ignored_file, normal_file, ignored_dir]
|
|
225
225
|
gitignore = treex.GitIgnore(tmp_path)
|
|
226
|
-
assert gitignore.ignored_many(paths) == {
|
|
226
|
+
assert gitignore.ignored_many(paths) == {ignored_file, ignored_dir}
|
|
227
227
|
|
|
228
228
|
def test_ignored_many_handles_spaces(self, tmp_path):
|
|
229
229
|
init_git_repo(tmp_path)
|
|
@@ -234,7 +234,7 @@ class TestGitIgnore:
|
|
|
234
234
|
ignored_dir.mkdir()
|
|
235
235
|
paths = [ignored_file, ignored_dir]
|
|
236
236
|
gitignore = treex.GitIgnore(tmp_path)
|
|
237
|
-
assert gitignore.ignored_many(paths) == {
|
|
237
|
+
assert gitignore.ignored_many(paths) == {ignored_file, ignored_dir}
|
|
238
238
|
|
|
239
239
|
def test_missing_git_index(self, tmp_path):
|
|
240
240
|
init_git_repo(tmp_path)
|
|
@@ -260,7 +260,7 @@ class TestTreeEntries:
|
|
|
260
260
|
(tmp_path / "Docs").mkdir()
|
|
261
261
|
(tmp_path / ".git").mkdir()
|
|
262
262
|
entries = treex._get_tree_entries(tmp_path, gitignore=None)
|
|
263
|
-
assert [entry.name for entry in entries] == [
|
|
263
|
+
assert [entry[0].name for entry in entries] == [
|
|
264
264
|
".git",
|
|
265
265
|
"Docs",
|
|
266
266
|
"src",
|
|
@@ -278,7 +278,7 @@ class TestTreeEntries:
|
|
|
278
278
|
link = tmp_path / "link.txt"
|
|
279
279
|
link.symlink_to(target)
|
|
280
280
|
entries = treex._get_tree_entries(tmp_path, gitignore=None)
|
|
281
|
-
assert [entry.name for entry in entries] == ["target.txt"]
|
|
281
|
+
assert [entry[0].name for entry in entries] == ["target.txt"]
|
|
282
282
|
|
|
283
283
|
|
|
284
284
|
class TestCollectTree:
|
|
@@ -380,7 +380,7 @@ class TestCollectTree:
|
|
|
380
380
|
def test_collect_tree_permission_denied(self, tmp_path):
|
|
381
381
|
rows = []
|
|
382
382
|
stats = {"directories": 0, "files": 0, "total_size": 0}
|
|
383
|
-
with patch
|
|
383
|
+
with patch("os.scandir", side_effect=PermissionError):
|
|
384
384
|
treex._collect_tree(tmp_path, "", stats, None, rows, True, False, True)
|
|
385
385
|
assert rows == [("└── [permission denied]", None)]
|
|
386
386
|
assert stats["directories"] == 0
|
|
@@ -562,6 +562,22 @@ class TestScanTree:
|
|
|
562
562
|
)
|
|
563
563
|
assert nodes == []
|
|
564
564
|
|
|
565
|
+
def test_relative_prefix_matches_nested_paths(self, tmp_path):
|
|
566
|
+
nested = tmp_path / "nested"
|
|
567
|
+
nested.mkdir()
|
|
568
|
+
(nested / "app.py").touch()
|
|
569
|
+
(nested / "notes.txt").touch()
|
|
570
|
+
nodes = treex._scan_tree(
|
|
571
|
+
nested,
|
|
572
|
+
None,
|
|
573
|
+
False,
|
|
574
|
+
False,
|
|
575
|
+
include_pattern="src/*.py",
|
|
576
|
+
relative_prefix="src",
|
|
577
|
+
)
|
|
578
|
+
assert [node["name"] for node in nodes] == ["app.py"]
|
|
579
|
+
assert nodes[0]["contains_match"] is True
|
|
580
|
+
|
|
565
581
|
|
|
566
582
|
class TestPrintTreeStatistics:
|
|
567
583
|
def test_counts_files_and_directories(self, tmp_path):
|
|
@@ -913,7 +929,7 @@ class TestPrintTree:
|
|
|
913
929
|
def test_reports_permission_denied(self, tmp_path, capsys):
|
|
914
930
|
directory = tmp_path / "restricted"
|
|
915
931
|
directory.mkdir()
|
|
916
|
-
with patch
|
|
932
|
+
with patch("os.scandir", side_effect=PermissionError):
|
|
917
933
|
treex.print_tree(directory)
|
|
918
934
|
assert "[permission denied]" in capsys.readouterr().out
|
|
919
935
|
|
|
@@ -1019,7 +1035,7 @@ class TestArgumentParsing:
|
|
|
1019
1035
|
treex.parse_args([option])
|
|
1020
1036
|
assert exc_info.value.code == 0
|
|
1021
1037
|
output = capsys.readouterr().out
|
|
1022
|
-
assert output.splitlines()[0] == "usage: treex [OPTIONS] [directory]"
|
|
1038
|
+
assert output.splitlines()[0] == f"usage: {treex.PROG} [OPTIONS] [directory]"
|
|
1023
1039
|
|
|
1024
1040
|
@pytest.mark.parametrize("option", ["-V", "--version"])
|
|
1025
1041
|
def test_version(self, option, capsys):
|
|
@@ -1027,7 +1043,7 @@ class TestArgumentParsing:
|
|
|
1027
1043
|
treex.parse_args([option])
|
|
1028
1044
|
assert exc_info.value.code == 0
|
|
1029
1045
|
output = capsys.readouterr().out
|
|
1030
|
-
assert output.strip() == f"treex {treex.__version__}"
|
|
1046
|
+
assert output.strip() == f"{treex.PROG} {treex.__version__}"
|
|
1031
1047
|
|
|
1032
1048
|
@pytest.mark.parametrize("option", ["-a", "--all"])
|
|
1033
1049
|
def test_all(self, option):
|
|
@@ -1164,7 +1180,7 @@ class TestArgumentParsing:
|
|
|
1164
1180
|
class TestMain:
|
|
1165
1181
|
def test_success(self, tmp_path, capsys):
|
|
1166
1182
|
(tmp_path / "file.txt").write_text("hello", encoding="utf-8")
|
|
1167
|
-
with patch.object(sys, "argv", [
|
|
1183
|
+
with patch.object(sys, "argv", [treex.PROG, str(tmp_path)]):
|
|
1168
1184
|
assert treex.main() == 0
|
|
1169
1185
|
output = capsys.readouterr().out
|
|
1170
1186
|
assert str(tmp_path) in output
|
|
@@ -1176,7 +1192,7 @@ class TestMain:
|
|
|
1176
1192
|
directory = tmp_path / "dir"
|
|
1177
1193
|
directory.mkdir()
|
|
1178
1194
|
(tmp_path / "file.txt").write_text("hello", encoding="utf-8")
|
|
1179
|
-
with patch.object(sys, "argv", [
|
|
1195
|
+
with patch.object(sys, "argv", [treex.PROG, "--dirs", str(tmp_path)]):
|
|
1180
1196
|
assert treex.main() == 0
|
|
1181
1197
|
output = capsys.readouterr().out
|
|
1182
1198
|
assert str(tmp_path) in output
|
|
@@ -1188,13 +1204,13 @@ class TestMain:
|
|
|
1188
1204
|
def test_not_a_directory(self, tmp_path, capsys):
|
|
1189
1205
|
path = tmp_path / "file.txt"
|
|
1190
1206
|
path.write_text("hello", encoding="utf-8")
|
|
1191
|
-
with patch.object(sys, "argv", [
|
|
1207
|
+
with patch.object(sys, "argv", [treex.PROG, str(path)]):
|
|
1192
1208
|
assert treex.main() == 1
|
|
1193
1209
|
assert f"Not a directory: {path}" in capsys.readouterr().err
|
|
1194
1210
|
|
|
1195
1211
|
def test_keyboard_interrupt(self, tmp_path, capsys):
|
|
1196
1212
|
with (
|
|
1197
|
-
patch.object(sys, "argv", [
|
|
1213
|
+
patch.object(sys, "argv", [treex.PROG, str(tmp_path)]),
|
|
1198
1214
|
patch.object(treex, "print_tree", side_effect=KeyboardInterrupt),
|
|
1199
1215
|
):
|
|
1200
1216
|
assert treex.main() == 130
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|