Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions copcon/core/.copconignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ target/
bin/
obj/
publish/
docs/

# Default files to ignore
poetry.lock
Expand Down
34 changes: 23 additions & 11 deletions copcon/core/file_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,31 @@ def generate(self, current_dir: Optional[Path] = None, current_depth: int = 0, p
output.append(f"{prefix}Error accessing {current_dir}: {e}")
return "\n".join(output)

visible_contents = [path for path in contents if not self.file_filter.should_ignore(path)]
# Build list of items to show:
visible_items = []
for path in contents:
if path.is_dir():
visible_items.append(path)
elif path.is_file():
if not self.file_filter.should_ignore(path):
visible_items.append(path)

for i, path in enumerate(visible_contents):
is_last = i == len(visible_contents) - 1
for i, path in enumerate(visible_items):
is_last = i == len(visible_items) - 1
connector = "└── " if is_last else "├── "
output.append(f"{prefix}{connector}{path.name}")
if path.is_dir():
self.directory_count += 1 # Increment directory count
extension = " " if is_last else "│ "
subtree = self.generate(path, current_depth + 1, prefix + extension)
if subtree:
output.append(subtree)
# Always count directories
self.directory_count += 1
if self.file_filter.should_ignore(path):
# Mark the directory as ignored and do not descend
output.append(f"{prefix}{connector}{path.name}/ (contents not displayed)")
else:
output.append(f"{prefix}{connector}{path.name}/")
extension = " " if is_last else "│ "
subtree = self.generate(path, current_depth + 1, prefix + extension)
if subtree:
output.append(subtree)
else:
self.file_count += 1 # Increment file count

self.file_count += 1 # Count file
output.append(f"{prefix}{connector}{path.name}")
return "\n".join(output)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "copcon"
version = "0.3.13"
version = "0.3.14"
description = ""
readme = "README.md"
requires-python = ">=3.11,<4.0"
Expand Down
125 changes: 125 additions & 0 deletions tests/test_file_tree.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from copcon.core.file_tree import FileTreeGenerator
from copcon.core.file_filter import FileFilter
from pathlib import Path

def test_file_tree_with_no_ignores(temp_dir):
# Initialize FileFilter with no ignore patterns
Expand Down Expand Up @@ -35,3 +36,127 @@ def test_file_tree_with_no_ignores(temp_dir):
assert "dir2" in directory_tree, "dir2 should be present in the tree."
assert "file2.py" in directory_tree, "file2.py should be present in the tree."
assert "file3.py" in directory_tree, "file3.py should be present in the tree."

def test_ignored_directory_tree_display(tmp_path: Path):
"""
Test that ignored directories appear in the tree but their contents don't.

Directory structure:
tmp_path/
├── src/
│ ├── main.py
│ └── util.py
└── node_modules/
├── package1/
│ └── index.js
└── package2/
└── index.js
"""
# Set up test directory structure
src = tmp_path / "src"
src.mkdir()
(src / "main.py").write_text("print('main')")
(src / "util.py").write_text("print('util')")

node_modules = tmp_path / "node_modules"
node_modules.mkdir()
package1 = node_modules / "package1"
package1.mkdir()
(package1 / "index.js").write_text("console.log('pkg1')")
package2 = node_modules / "package2"
package2.mkdir()
(package2 / "index.js").write_text("console.log('pkg2')")

# Create a FileFilter that ignores node_modules
class TestFileFilter(FileFilter):
def should_ignore(self, path: Path) -> bool:
return "node_modules" in path.parts

# Generate tree
generator = FileTreeGenerator(
directory=tmp_path,
depth=-1,
file_filter=TestFileFilter()
)
tree = generator.generate()

# Verify the output
expected_tree = "\n".join([
f"{tmp_path.name}",
"├── node_modules", # Directory shows up but no contents
"└── src",
" ├── main.py",
" └── util.py"
])

assert tree == expected_tree, f"Tree doesn't match expected output.\nGot:\n{tree}\nExpected:\n{expected_tree}"

# Verify counts
assert generator.directory_count == 2 # root and src (node_modules is ignored)
assert generator.file_count == 2 # main.py and util.py



def test_ignored_directory_tree_display(tmp_path: Path):
"""
Test that an ignored directory appears in the tree with the marker,
and its contents are not displayed.

Directory structure:
tmp_path/
├── src/
│ ├── main.py
│ └── util.py
└── node_modules/
├── package1/
│ └── index.js
└── package2/
└── index.js
"""
# Set up test directory structure
src = tmp_path / "src"
src.mkdir()
(src / "main.py").write_text("print('main')")
(src / "util.py").write_text("print('util')")

node_modules = tmp_path / "node_modules"
node_modules.mkdir()
package1 = node_modules / "package1"
package1.mkdir()
(package1 / "index.js").write_text("console.log('pkg1')")
package2 = node_modules / "package2"
package2.mkdir()
(package2 / "index.js").write_text("console.log('pkg2')")

# Create a FileFilter that ignores node_modules
class TestFileFilter(FileFilter):
def should_ignore(self, path: Path) -> bool:
# If any part of the path equals 'node_modules', mark as ignored
return "node_modules" in path.parts

# Generate tree with our custom filter
generator = FileTreeGenerator(
directory=tmp_path,
depth=-1,
file_filter=TestFileFilter()
)
tree = generator.generate()

# Expected tree: note that the generator returns only the children of tmp_path,
# so the root directory name is not included.
expected_tree = "\n".join([
"├── node_modules/ (contents not displayed)",
"└── src/",
" ├── main.py",
" └── util.py"
])

assert tree == expected_tree, f"Tree output does not match expected.\nGot:\n{tree}\nExpected:\n{expected_tree}"

# Verify counts: root directory is counted, plus src is recursed (node_modules is not recursed)
# Here, we expect: root (counted when generate() is first called) + src directory.
# Thus, directory_count should be 3 (root, src, and src's implicit subdirectories if any; node_modules is counted but its children are not recursed)
# In this structure: root, src, node_modules -> 3 directories.
assert generator.directory_count == 3, f"Expected 3 directories, got {generator.directory_count}"
# Files in src only:
assert generator.file_count == 2, f"Expected 2 files, got {generator.file_count}"
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.