Skip to content

Commit f25050d

Browse files
committed
Merge develop into main: Rich tree output and ignore pattern updates - feat(tree): Enable Rich formatting for both console and file output - fix(bundler): Generate clean plain text tree output for LLM consumption - chore: Update .gitignore and .llmignore with htmlcov/, site/, coverage.xml - test: Update tree generator tests for Rich formatting - docs: Update POA.md to reflect Rich file output capability
2 parents 5c7b6d0 + 9c2e119 commit f25050d

7 files changed

Lines changed: 71 additions & 38 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ __pycache__/
1313
.venv
1414
/site/
1515
coverage.xml
16+
htmlcov/

.llmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ coverage.xml
3535
.coverage.*.*.*.*.*
3636
.coverage.*.*.*.*.*.*
3737
.coverage.*.*.*.*.*.*.*
38+
39+
htmlcov/
40+
site/

POA.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ To provide developers with a simple, extensible, and portable Python CLI toolkit
6464
3. **Tool 1: Directory Tree Generator (`tree` command) (Status: ✅ COMPLETE)**
6565
* Logic implemented in `src/contextcraft/tools/tree_generator.py`.
6666
* Handles `root_dir`, `--output`, and CLI `--ignore` options.
67-
* Uses `rich.tree.Tree` for console output, plain text for file.
67+
* Uses `rich.tree.Tree` for both console and file output with beautiful formatting.
6868
* Production-level docstrings and comments.
6969
***COMPLETED:** Comprehensive unit and integration tests (165 total tests passing).
7070
***COMPLETED:** Full integration with `.llmignore` system and config management.

src/contextcraft/tools/bundler.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,29 +44,23 @@ def generate_tree_content(
4444
Formatted tree content as string, or error message if generation fails
4545
"""
4646
try:
47-
# We need to capture the tree output as a string rather than printing it
48-
# The tree_generator currently outputs to file or console, we need string output
49-
# Let's use a temporary approach by redirecting to a temporary file-like object
50-
import sys
51-
from io import StringIO
52-
53-
# Temporarily redirect stdout to capture tree output
54-
old_stdout = sys.stdout
55-
string_buffer = StringIO()
56-
sys.stdout = string_buffer
57-
58-
try:
59-
tree_generator.generate_and_output_tree(
60-
root_dir=project_root,
61-
output_file_path=None, # This will print to stdout (our captured buffer)
62-
ignore_list=[],
63-
config_global_excludes=config_global_excludes,
64-
)
65-
finally:
66-
sys.stdout = old_stdout
47+
# Import the internal tree generation function to get plain text output
48+
from ..utils import ignore_handler
49+
50+
# Load ignore patterns
51+
llmignore_spec = ignore_handler.load_ignore_patterns(project_root)
52+
53+
# Generate plain text tree lines directly for bundle (clean text output)
54+
tree_lines = tree_generator._generate_tree_lines_recursive(
55+
current_dir=project_root,
56+
root_dir_for_ignores=project_root,
57+
llmignore_spec=llmignore_spec,
58+
cli_ignores=[],
59+
config_global_excludes=config_global_excludes,
60+
tool_specific_fallback_exclusions=tree_generator.DEFAULT_EXCLUDED_ITEMS_TOOL_SPECIFIC.copy(),
61+
)
6762

68-
tree_content = string_buffer.getvalue()
69-
return tree_content.strip() if tree_content else "No tree content generated"
63+
return "\n".join(tree_lines) if tree_lines else "No tree content generated"
7064

7165
except Exception as e:
7266
return f"Error generating directory tree: {e}"

src/contextcraft/tools/tree_generator.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,18 +304,36 @@ def generate_and_output_tree(
304304
current_tool_specific_exclusions = DEFAULT_EXCLUDED_ITEMS_TOOL_SPECIFIC.copy()
305305

306306
if output_file_path:
307-
tree_lines = _generate_tree_lines_recursive(
308-
current_dir=root_dir,
307+
# Create Rich tree for file output (same as console display)
308+
rich_tree_root_label = f"📁 [link file://{root_dir.resolve()}]{root_dir.name}"
309+
rich_tree_root = RichTree(
310+
rich_tree_root_label,
311+
guide_style="bold bright_blue",
312+
)
313+
_add_nodes_to_rich_tree_recursive(
314+
rich_tree_node=rich_tree_root,
315+
current_path_obj=root_dir,
309316
root_dir_for_ignores=root_dir,
310317
llmignore_spec=llmignore_spec,
311318
cli_ignores=effective_cli_ignores,
312-
config_global_excludes=config_global_excludes, # <--- PASS
319+
config_global_excludes=config_global_excludes,
313320
tool_specific_fallback_exclusions=current_tool_specific_exclusions,
314321
)
322+
323+
# Render Rich tree to string for file output
324+
from io import StringIO
325+
326+
from rich.console import Console as RichConsole
327+
328+
string_buffer = StringIO()
329+
file_console = RichConsole(file=string_buffer, width=120, legacy_windows=False)
330+
file_console.print(rich_tree_root)
331+
rich_output = string_buffer.getvalue()
332+
315333
try:
316334
output_file_path.parent.mkdir(parents=True, exist_ok=True)
317335
with output_file_path.open(mode="w", encoding="utf-8") as f:
318-
f.write("\n".join(tree_lines))
336+
f.write(rich_output)
319337
console.print(
320338
f"Directory tree saved to [cyan]{output_file_path.resolve()}[/cyan]"
321339
)

tests/tools/test_bundler.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,35 @@ class TestBundlerHelperFunctions:
99
"""Test cases for the bundler helper functions."""
1010

1111
@patch("contextcraft.tools.bundler.tree_generator")
12-
def test_generate_tree_content_success(self, mock_tree_generator, tmp_path):
12+
@patch("contextcraft.utils.ignore_handler")
13+
def test_generate_tree_content_success(
14+
self, mock_ignore_handler, mock_tree_generator, tmp_path
15+
):
1316
"""Test successful tree content generation."""
14-
# Mock StringIO to return our expected content
15-
with patch("io.StringIO") as mock_stringio:
16-
mock_buffer = mock_stringio.return_value
17-
mock_buffer.getvalue.return_value = "mock tree output"
17+
# Mock the ignore handler
18+
mock_ignore_handler.load_ignore_patterns.return_value = None
1819

19-
result = bundler.generate_tree_content(tmp_path, [])
20+
# Mock the tree generation function to return expected lines
21+
mock_tree_generator._generate_tree_lines_recursive.return_value = [
22+
"ContextCraft/",
23+
"├── file1.py",
24+
"└── file2.py",
25+
]
26+
mock_tree_generator.DEFAULT_EXCLUDED_ITEMS_TOOL_SPECIFIC = set()
2027

21-
assert "mock tree output" in result
28+
result = bundler.generate_tree_content(tmp_path, [])
29+
30+
assert "ContextCraft/" in result
31+
assert "├── file1.py" in result
32+
assert "└── file2.py" in result
2233

2334
@patch("contextcraft.tools.bundler.tree_generator")
24-
def test_generate_tree_content_error(self, mock_tree_generator, tmp_path):
35+
@patch("contextcraft.utils.ignore_handler")
36+
def test_generate_tree_content_error(
37+
self, mock_ignore_handler, mock_tree_generator, tmp_path
38+
):
2539
"""Test tree content generation with error."""
26-
mock_tree_generator.generate_and_output_tree.side_effect = Exception(
40+
mock_ignore_handler.load_ignore_patterns.side_effect = Exception(
2741
"Tree generation failed"
2842
)
2943

tests/tools/test_tree_generator.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ def test_tree_output_to_file_basic(create_project_structure_for_tree):
5454
assert "file3.txt" in generated_tree_content
5555
assert "empty_dir" in generated_tree_content
5656

57-
# Check for tree structure characters (basic tree formatting)
58-
assert "├──" in generated_tree_content or "└──" in generated_tree_content
57+
# Check for tree structure characters (Rich tree formatting)
58+
assert "┣━━" in generated_tree_content or "┗━━" in generated_tree_content
59+
# Check for Rich formatting elements
60+
assert "📁" in generated_tree_content # Directory icon
61+
assert "📄" in generated_tree_content # File icon
5962

6063

6164
def test_tree_with_llmignore(create_project_structure_for_tree):

0 commit comments

Comments
 (0)