Private
Public Access
0
0
Files
manual_slop/src/outline_tool.py
T
ed a7d8e2adfd refactor(src): Phase 10.2 batch 2 - outline_tool Result[T] migration
Migrates 3 sites in src/outline_tool.py:
1. L49 (outline body) - the ast.parse SyntaxError handler.
   outline() now returns Result[str]. On SyntaxError, the data
   is the formatted error string (preserved for backwards-compat
   with callers that read the formatted string), and the errors
   list has the ErrorInfo.
2. L90 (walk ast.unparse for returns) - was except ...: pass.
   Now appends ErrorInfo to enclosing parse_errors list.
3. L109 (walk ast.unparse for ImGui context) - same.

outline() returns Result(data='\n'.join(output), errors=parse_errors).
get_outline() also returns Result[str].

Tests updated to check result.ok and use result.data.
2026-06-17 22:31:35 -04:00

133 lines
4.6 KiB
Python

"""
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
from src.result_types import Result, ErrorInfo, ErrorKind
class CodeOutliner:
def __init__(self) -> None:
pass
def outline(self, code: str) -> Result[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 Result(data=f"ERROR parsing code: {e}", errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=str(e), source="outline_tool.outline", original=e)])
output: list[str] = []
parse_errors: list[ErrorInfo] = []
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 (ValueError, TypeError) as e:
parse_errors.append(ErrorInfo(kind=ErrorKind.INTERNAL, message=f"ast.unparse failed for {node.name}.returns: {e}", source="outline_tool.walk", original=e))
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 (ValueError, TypeError, AttributeError) as e:
parse_errors.append(ErrorInfo(kind=ErrorKind.INTERNAL, message=f"ast.unparse failed for ImGui context: {e}", source="outline_tool.walk", original=e))
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 Result(data="\n".join(output), errors=parse_errors)
def get_outline(path: Path, code: str) -> Result[str]:
suffix = path.suffix.lower()
if suffix == ".py":
outliner = CodeOutliner()
return outliner.outline(code)
else:
return Result(data=f"Outlining not supported for {suffix} files yet.")