diff --git a/Projects/repo-mapper/README.md b/Projects/repo-mapper/README.md new file mode 100644 index 0000000..5bee25b --- /dev/null +++ b/Projects/repo-mapper/README.md @@ -0,0 +1,12 @@ +# Repo-to-Knowledge Mapper +A lightweight Python static analysis tool that generates a structured Markdown map of your project's architecture. + +## Purpose +Onboarding to a new codebase is hard. This script scans a directory and extracts a high-level overview of classes and public methods using Python's Abstract Syntax Tree (AST), helping developers understand "what lives where" without manual auditing. + +## Installation +1. Clone the repository. +2. Install testing dependencies: + + ```bash + pip install -r requirements.txt \ No newline at end of file diff --git a/Projects/repo-mapper/mapper.py b/Projects/repo-mapper/mapper.py new file mode 100644 index 0000000..ad8e533 --- /dev/null +++ b/Projects/repo-mapper/mapper.py @@ -0,0 +1,51 @@ +import ast +import os +import argparse +from typing import Dict, List + +class RepoAnalyzer(ast.NodeVisitor): + def __init__(self): + self.stats = {"classes": [], "functions": []} + + def visit_ClassDef(self, node: ast.ClassDef): + self.stats["classes"].append(node.name) + self.generic_visit(node) + + def visit_FunctionDef(self, node: ast.FunctionDef): + # Ignore private methods/functions + if not node.name.startswith('_'): + self.stats["functions"].append(node.name) + self.generic_visit(node) + +def analyze_file(filepath: str) -> Dict[str, List[str]]: + with open(filepath, "r", encoding="utf-8") as f: + try: + tree = ast.parse(f.read()) + analyzer = RepoAnalyzer() + analyzer.visit(tree) + return analyzer.stats + except (SyntaxError, UnicodeDecodeError): + return {"classes": [], "functions": []} + +def run_mapper(target_dir: str): + print(f"# Project Map: {os.path.abspath(target_dir)}\n") + for root, _, files in os.walk(target_dir): + for file in files: + if file.endswith(".py"): + path = os.path.join(root, file) + rel_path = os.path.relpath(path, target_dir) + data = analyze_file(path) + + if data["classes"] or data["functions"]: + print(f"### `{rel_path}`") + if data["classes"]: + print(f"- **Classes**: {', '.join(data['classes'])}") + if data["functions"]: + print(f"- **Public Methods**: {', '.join(data['functions'])}") + print() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate a Markdown map of a Python repository.") + parser.add_argument("path", nargs="?", default=".", help="Directory to analyze (default: current)") + args = parser.parse_args() + run_mapper(args.path) \ No newline at end of file diff --git a/Projects/repo-mapper/requirements.txt b/Projects/repo-mapper/requirements.txt new file mode 100644 index 0000000..fd35639 --- /dev/null +++ b/Projects/repo-mapper/requirements.txt @@ -0,0 +1 @@ +pytest==8.0.0 \ No newline at end of file diff --git a/Projects/repo-mapper/tests/test_mapper.py b/Projects/repo-mapper/tests/test_mapper.py new file mode 100644 index 0000000..bd28bd8 --- /dev/null +++ b/Projects/repo-mapper/tests/test_mapper.py @@ -0,0 +1,23 @@ +import pytest +import os +from mapper import analyze_file + +def test_analyze_simple_code(tmp_path): + # Create a temporary python file + d = tmp_path / "sub" + d.mkdir() + p = d / "hello.py" + p.write_text("class MyClass:\n def my_method(self):\n pass\n\ndef my_function():\n pass") + + results = analyze_file(str(p)) + + assert "MyClass" in results["classes"] + assert "my_method" in results["functions"] + assert "my_function" in results["functions"] + +def test_ignore_private_methods(tmp_path): + p = tmp_path / "private.py" + p.write_text("def _hidden():\n pass") + + results = analyze_file(str(p)) + assert "_hidden" not in results["functions"] \ No newline at end of file