""" Outline Tool - Hierarchical code outline extraction via stdlib ast. This module provides the CodeOutliner class for generating a hierarchical outline of Python source code, showing classes, methods, and functions with their line ranges and docstrings. Key Features: - Uses Python's built-in ast module (no external dependencies) - Extracts class and function definitions with line ranges - Includes first line of docstrings for each definition - Distinguishes between methods and top-level functions Usage: outliner = CodeOutliner() outline = outliner.outline(python_code) Output Format: [Class] ClassName (Lines 10-50) 'First line of class docstring' [Method] __init__ (Lines 11-20) [Method] process (Lines 22-35) [Func] top_level_function (Lines 55-70) Integration: - Used by mcp_client.py for py_get_code_outline tool - Used by simulation tests for code structure verification See Also: - src/file_cache.py for ASTParser (tree-sitter based) - src/summarize.py for heuristic file summaries """ import ast from pathlib import Path class CodeOutliner: def __init__(self) -> None: pass def outline(self, code: str) -> str: """ [C: tests/test_outline_tool.py:test_code_outliner_imgui_scopes, tests/test_outline_tool.py:test_code_outliner_nested_ifs, tests/test_outline_tool.py:test_code_outliner_type_hints] """ code = code.lstrip(chr(0xFEFF)) try: tree = ast.parse(code) except SyntaxError as e: return f"ERROR parsing code: {e}" output = [] def get_docstring(node: ast.AST) -> str | None: if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, ast.Module)): doc = ast.get_docstring(node) if doc: return doc.splitlines()[0] return None count = [0] def walk(node: ast.AST, indent: int = 0) -> None: """ [C: src/summarize.py:_summarise_python] """ count[0] += 1 if count[0] > 100000: raise Exception("Infinite loop detected! " + str(type(node))) """ [C: src/summarize.py:_summarise_python] """ if isinstance(node, ast.ClassDef): start_line = node.lineno end_line = getattr(node, "end_lineno", start_line) output.append(f"{' ' * indent}[Class] {node.name} (Lines {start_line}-{end_line})") doc = get_docstring(node) if doc: output.append(f"{' ' * (indent + 1)}\"\"\"{doc}\"\"\"") for item in node.body: walk(item, indent + 1) elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): start_line = node.lineno end_line = getattr(node, "end_lineno", start_line) prefix = "[Async Func]" if isinstance(node, ast.AsyncFunctionDef) else "[Func]" if indent > 0: prefix = "[Method]" returns = "" if getattr(node, "returns", None): try: returns = f" -> {ast.unparse(node.returns)}" except Exception: pass output.append(f"{' ' * indent}{prefix} {node.name}{returns} (Lines {start_line}-{end_line})") doc = get_docstring(node) if doc: output.append(f"{' ' * (indent + 1)}\"\"\"{doc}\"\"\"") for item in node.body: walk(item, indent + 1) elif isinstance(node, ast.With): is_imgui = False try: for item in node.items: ctx_str = ast.unparse(item.context_expr) if "imscope." in ctx_str or "imgui." in ctx_str: start_line = node.lineno end_line = getattr(node, "end_lineno", start_line) output.append(f"{' ' * indent}[ImGui Scope] {ctx_str} (Lines {start_line}-{end_line})") is_imgui = True break except Exception: pass for item in node.body: walk(item, indent + 1 if is_imgui else indent) else: for block_attr in ("body", "orelse", "handlers", "finalbody"): block = getattr(node, block_attr, []) if isinstance(block, list): for item in block: walk(item, indent) for node in tree.body: walk(node) return "\n".join(output) def get_outline(path: Path, code: str) -> str: suffix = path.suffix.lower() if suffix == ".py": outliner = CodeOutliner() return outliner.outline(code) else: return f"Outlining not supported for {suffix} files yet."