Skip to content

Commit 22dc631

Browse files
committed
Add .treemapper/ config directory support for ignore and whitelist
1 parent b83515c commit 22dc631

4 files changed

Lines changed: 164 additions & 10 deletions

File tree

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
.pytest_cache
44
.coverage
55
htmlcov
6-
.treemapperignore
76
*.yaml
87
*.yml
98
tests

src/treemapper/cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,12 @@ class ParsedArgs:
127127
128128
Ignore files (hierarchical, like git):
129129
.gitignore Standard git ignore patterns
130-
.treemapperignore TreeMapper-specific patterns
130+
.treemapper/ignore TreeMapper-specific patterns (preferred)
131+
.treemapperignore TreeMapper-specific patterns (legacy)
132+
133+
Whitelist files (auto-discovered):
134+
.treemapper/whitelist Include-only filter (preferred)
135+
.treemapperwhitelist Include-only filter (legacy)
131136
"""
132137

133138

src/treemapper/ignore.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
import pathspec
88

9+
TREEMAPPER_CONFIG_DIR = ".treemapper"
10+
TREEMAPPER_DIR_IGNORE = "ignore"
11+
TREEMAPPER_DIR_WHITELIST = "whitelist"
12+
913

1014
def read_ignore_file(file_path: Path) -> list[str]:
1115
ignore_patterns: list[str] = []
@@ -57,6 +61,7 @@ def _get_output_file_pattern(output_file: Path | None, root_dir: Path) -> str |
5761
".git",
5862
".svn",
5963
".hg",
64+
".treemapper",
6065
"__pycache__",
6166
"node_modules",
6267
".npm",
@@ -105,17 +110,19 @@ def _aggregate_all_ignore_patterns(root: Path, ignore_filenames: list[str]) -> l
105110
for dirpath, dirnames, filenames in os.walk(root, topdown=True):
106111
dirnames[:] = sorted(d for d in dirnames if d not in PRUNE_DIRS and not _is_cache_dir(d))
107112

108-
found_files = filenames_set & set(filenames)
109-
if not found_files:
110-
continue
111-
112113
ignore_dir = Path(dirpath)
113114
rel = "" if ignore_dir == root else ignore_dir.relative_to(root).as_posix()
114115

116+
found_files = filenames_set & set(filenames)
115117
for ignore_filename in sorted(found_files):
116118
for line in read_ignore_file(ignore_dir / ignore_filename):
117119
out.append(_process_ignore_line(line, rel))
118120

121+
config_ignore = ignore_dir / TREEMAPPER_CONFIG_DIR / TREEMAPPER_DIR_IGNORE
122+
if config_ignore.is_file():
123+
for line in read_ignore_file(config_ignore):
124+
out.append(_process_ignore_line(line, rel))
125+
119126
logging.debug("Aggregated %d ignore patterns from %s", len(out), root)
120127
return out
121128

@@ -184,6 +191,9 @@ def _collect_parent_ignore_patterns(root: Path, ignore_filenames: list[str]) ->
184191
for filename in ignore_filenames:
185192
_process_parent_ignore_file(current / filename, resolved_root, current, out)
186193

194+
config_ignore = current / TREEMAPPER_CONFIG_DIR / TREEMAPPER_DIR_IGNORE
195+
_process_parent_ignore_file(config_ignore, resolved_root, current, out)
196+
187197
if is_git_root:
188198
break
189199

@@ -232,7 +242,8 @@ def _collect_parent_ignore_patterns(root: Path, ignore_filenames: list[str]) ->
232242
# OS files
233243
"**/.DS_Store",
234244
"**/Thumbs.db",
235-
# TreeMapper default output files
245+
# TreeMapper config and output files
246+
"**/.treemapper/",
236247
"**/tree.yaml",
237248
"**/tree.yml",
238249
"**/tree.json",
@@ -280,9 +291,13 @@ def should_ignore(relative_path_str: str, combined_spec: pathspec.PathSpec) -> b
280291
def get_whitelist_spec(whitelist_file: Path | None, root_dir: Path | None = None) -> pathspec.PathSpec | None:
281292
effective_file = whitelist_file
282293
if not effective_file and root_dir:
283-
default = root_dir / DEFAULT_WHITELIST_FILENAME
284-
if default.is_file():
285-
effective_file = default
294+
config_whitelist = root_dir / TREEMAPPER_CONFIG_DIR / TREEMAPPER_DIR_WHITELIST
295+
if config_whitelist.is_file():
296+
effective_file = config_whitelist
297+
else:
298+
default = root_dir / DEFAULT_WHITELIST_FILENAME
299+
if default.is_file():
300+
effective_file = default
286301
if not effective_file:
287302
return None
288303
patterns = read_ignore_file(effective_file)

tests/test_ignore.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,138 @@ def test_parent_ignore_stops_at_git_root(tmp_path, run_mapper):
534534
assert find_node_by_path(result, ["beyond_git_root"]) is not None
535535
assert find_node_by_path(result, ["beyond_git_root", "visible.txt"]) is not None
536536
assert find_node_by_path(result, ["keep.txt"]) is not None
537+
538+
539+
# --- .treemapper/ config directory tests ---
540+
541+
542+
def test_treemapper_dir_ignore(temp_project, run_mapper):
543+
from .utils import find_node_by_path
544+
545+
config_dir = temp_project / ".treemapper"
546+
config_dir.mkdir(exist_ok=True)
547+
(config_dir / "ignore").write_text("*.log\ndocs/\n")
548+
(temp_project / "app.log").touch()
549+
(temp_project / "keep.txt").touch()
550+
551+
output_path = temp_project / "dir_ignore_output.yaml"
552+
assert run_mapper([".", "-o", str(output_path)])
553+
result = load_yaml(output_path)
554+
555+
assert find_node_by_path(result, ["app.log"]) is None
556+
assert find_node_by_path(result, ["docs"]) is None
557+
assert find_node_by_path(result, ["keep.txt"]) is not None
558+
559+
560+
def test_treemapper_dir_ignore_combined_with_legacy(temp_project, run_mapper):
561+
from .utils import find_node_by_path
562+
563+
(temp_project / ".treemapperignore").write_text("*.tmp\n")
564+
config_dir = temp_project / ".treemapper"
565+
config_dir.mkdir(exist_ok=True)
566+
(config_dir / "ignore").write_text("*.bak\n")
567+
(temp_project / "file.tmp").touch()
568+
(temp_project / "file.bak").touch()
569+
(temp_project / "file.txt").touch()
570+
571+
output_path = temp_project / "combined_dir_output.yaml"
572+
assert run_mapper([".", "-o", str(output_path)])
573+
result = load_yaml(output_path)
574+
575+
assert find_node_by_path(result, ["file.tmp"]) is None
576+
assert find_node_by_path(result, ["file.bak"]) is None
577+
assert find_node_by_path(result, ["file.txt"]) is not None
578+
579+
580+
def test_treemapper_dir_ignore_hierarchical(temp_project, run_mapper):
581+
from .utils import find_node_by_path
582+
583+
subdir = temp_project / "subdir"
584+
subdir.mkdir()
585+
sub_config = subdir / ".treemapper"
586+
sub_config.mkdir()
587+
(sub_config / "ignore").write_text("*.secret\n")
588+
589+
(temp_project / "root.secret").touch()
590+
(subdir / "nested.secret").touch()
591+
(subdir / "keep.txt").touch()
592+
593+
output_path = temp_project / "hier_dir_output.yaml"
594+
assert run_mapper([".", "-o", str(output_path)])
595+
result = load_yaml(output_path)
596+
597+
assert find_node_by_path(result, ["root.secret"]) is not None
598+
assert find_node_by_path(result, ["subdir", "nested.secret"]) is None
599+
assert find_node_by_path(result, ["subdir", "keep.txt"]) is not None
600+
601+
602+
def test_treemapper_dir_ignore_parent(tmp_path, run_mapper):
603+
from .utils import find_node_by_path
604+
605+
parent = tmp_path / "parent_project"
606+
parent.mkdir()
607+
(parent / ".git").mkdir()
608+
config_dir = parent / ".treemapper"
609+
config_dir.mkdir()
610+
(config_dir / "ignore").write_text("packages/app/secret_dir/\n")
611+
612+
child = parent / "packages" / "app"
613+
child.mkdir(parents=True)
614+
(child / "secret_dir").mkdir()
615+
(child / "secret_dir" / "secret.txt").write_text("secret")
616+
(child / "src").mkdir()
617+
(child / "src" / "main.py").write_text("print('hello')")
618+
619+
output_path = tmp_path / "parent_dir_output.yaml"
620+
assert run_mapper([str(child), "-o", str(output_path)])
621+
result = load_yaml(output_path)
622+
623+
assert find_node_by_path(result, ["secret_dir"]) is None
624+
assert find_node_by_path(result, ["src", "main.py"]) is not None
625+
626+
627+
def test_treemapper_dir_whitelist(temp_project, run_mapper):
628+
config_dir = temp_project / ".treemapper"
629+
config_dir.mkdir(exist_ok=True)
630+
(config_dir / "whitelist").write_text("src/**/*.py\n")
631+
632+
output_path = temp_project / "dir_wl_output.yaml"
633+
assert run_mapper([".", "-o", str(output_path)])
634+
result = load_yaml(output_path)
635+
all_files = get_all_files_in_tree(result)
636+
637+
assert "main.py" in all_files
638+
assert "test.py" in all_files
639+
assert "readme.md" not in all_files
640+
641+
642+
def test_treemapper_dir_hidden_from_output(temp_project, run_mapper):
643+
config_dir = temp_project / ".treemapper"
644+
config_dir.mkdir(exist_ok=True)
645+
(config_dir / "ignore").write_text("*.log\n")
646+
(config_dir / "whitelist").write_text("src/**\n")
647+
(temp_project / "keep.txt").touch()
648+
649+
output_path = temp_project / "hidden_dir_output.yaml"
650+
assert run_mapper([".", "-o", str(output_path)])
651+
result = load_yaml(output_path)
652+
all_files = get_all_files_in_tree(result)
653+
654+
assert ".treemapper" not in all_files
655+
assert "ignore" not in all_files
656+
assert "whitelist" not in all_files
657+
658+
659+
def test_treemapper_dir_whitelist_over_legacy(temp_project, run_mapper):
660+
(temp_project / ".treemapperwhitelist").write_text("docs/**\n")
661+
config_dir = temp_project / ".treemapper"
662+
config_dir.mkdir(exist_ok=True)
663+
(config_dir / "whitelist").write_text("src/**/*.py\n")
664+
665+
output_path = temp_project / "wl_precedence_output.yaml"
666+
assert run_mapper([".", "-o", str(output_path)])
667+
result = load_yaml(output_path)
668+
all_files = get_all_files_in_tree(result)
669+
670+
assert "main.py" in all_files
671+
assert "readme.md" not in all_files

0 commit comments

Comments
 (0)