diff --git a/Makefile b/Makefile index 83b5c82..7db7421 100644 --- a/Makefile +++ b/Makefile @@ -46,3 +46,6 @@ docker: app: uv run python -m change_me + +tree: + uv run python repo_tree.py --update-readme diff --git a/README.md b/README.md index aadca7f..3a8c6db 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,33 @@ if __name__ == "__main__": ```bash uv run python -m change_me ``` + +## Structure +The following tree shows the important permanent files. + +``` +├── src +│ └── change_me +│ ├── __init__.py +│ ├── __main__.py +│ ├── definitions.py +│ └── utils.py +├── tests +│ ├── __init__.py +│ ├── conftest.py +│ ├── main_test.py +│ └── utils_test.py +├── .dockerignore +├── .gitignore +├── .pre-commit-config.yaml +├── .python-version +├── CONTRIBUTING.md +├── Dockerfile +├── LICENSE +├── Makefile +├── README.md +├── pyproject.toml +├── repo_tree.py +└── uv.lock +``` + diff --git a/repo_tree.py b/repo_tree.py new file mode 100644 index 0000000..3f35260 --- /dev/null +++ b/repo_tree.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Generate a Markdown tree of the project directory. + +Usage: + python repo_tree.py + python repo_tree.py --update-readme +""" + +from __future__ import annotations + +import argparse +import fnmatch +from pathlib import Path + +from loguru import logger + +START_MARKER = "" +END_MARKER = "" + +# Manual ignores are still allowed +MANUAL_IGNORE = { + ".git", + ".venv", + "__pycache__", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".DS_Store", + "data", + "simulink", + ".github", +} + + +def load_gitignore_patterns(path: Path) -> list[str]: + """Load patterns from .gitignore.""" + gitignore = path / ".gitignore" + if not gitignore.exists(): + return [] + + patterns: list[str] = [] + for line in gitignore.read_text().splitlines(): + line_str = line.strip() + if not line_str or line_str.startswith("#"): + continue + patterns.append(line_str) + return patterns + + +def is_ignored(path: Path, patterns: list[str]) -> bool: + """Return True if path matches any ignore pattern. + + Supports: + - literal names + - wildcard patterns + - directory patterns (e.g. logs/) + """ + name = path.name + + # Manual ignores first + if name in MANUAL_IGNORE: + return True + + for pattern in patterns: + # Directory ignore + if pattern.endswith("/") and path.is_dir(): + if fnmatch.fnmatch(name + "/", pattern): + return True + + # File/directory wildcard + if fnmatch.fnmatch(name, pattern): + return True + + return False + + +def build_tree(path: Path, patterns: list[str], prefix: str = "") -> list[str]: + """Recursively build a tree representation.""" + entries = sorted( + [p for p in path.iterdir() if not is_ignored(p, patterns)], + key=lambda x: (x.is_file(), x.name), + ) + + lines: list[str] = [] + + for i, entry in enumerate(entries): + connector = "└── " if i == len(entries) - 1 else "├── " + + lines.append(f"{prefix}{connector}{entry.name}") + + if entry.is_dir(): + extension = " " if i == len(entries) - 1 else "│ " + lines.extend(build_tree(entry, patterns, prefix + extension)) + + return lines + + +def generate_markdown_tree() -> str: + """Return the full markdown tree as a formatted code block.""" + root = Path(".").resolve() + + gitignore_patterns = load_gitignore_patterns(root) + tree_lines = build_tree(root, gitignore_patterns) + tree_text = "\n".join(tree_lines) + + return f"```\n{tree_text}\n```" + + +def update_readme_block(readme_path: Path) -> None: + """Replace the section between markers with the generated tree.""" + readme = readme_path.read_text().splitlines() + + if START_MARKER not in readme or END_MARKER not in readme: + raise RuntimeError( + f"README.md must contain '{START_MARKER}' and '{END_MARKER}' markers." + ) + + start = readme.index(START_MARKER) + 1 + end = readme.index(END_MARKER) + + tree = generate_markdown_tree().splitlines() + + new_readme = readme[:start] + tree + readme[end:] + readme_path.write_text("\n".join(new_readme)) + + logger.success( + "✅ README updated with latest repo tree (gitignored files excluded)." + ) + + +def main() -> None: + """Create a Markdown tree of the project directory.""" + parser = argparse.ArgumentParser() + parser.add_argument("--update-readme", action="store_true") + args = parser.parse_args() + + if args.update_readme: + update_readme_block(Path("README.md")) + else: + logger.info(generate_markdown_tree()) + + +if __name__ == "__main__": + main() diff --git a/src/change_me/config/__init__.py b/src/change_me/config/__init__.py deleted file mode 100644 index a22f654..0000000 --- a/src/change_me/config/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Sample doc string."""