a5b40bcff4
Migrates the 8 try/except sites in Infrastructure + Hook + Utility
files by narrowing the exception types from broad 'except Exception'
to specific stdlib/domain exceptions.
Files and sites:
1. src/api_hooks.py:453 (HookHandler.do_GET error response)
except Exception -> except (OSError, ValueError)
2. src/api_hooks.py:826 (HookHandler.do_POST error response)
except Exception -> except (OSError, ValueError)
3. src/api_hooks.py:916 (websocket connection cleanup)
except Exception -> except (OSError, ValueError)
4. src/file_cache.py:84 (path mtime stat)
except Exception -> except (OSError, ValueError)
5. src/orchestrator_pm.py:37 (track metadata.json read)
except Exception -> except (OSError, json.JSONDecodeError, UnicodeDecodeError)
6. src/orchestrator_pm.py:49 (track spec.md read)
except Exception -> except (OSError, UnicodeDecodeError)
7. src/outline_tool.py:67 (ast.unparse node.returns)
except Exception -> except (ValueError, TypeError)
8. src/outline_tool.py:90 (ast.unparse ImGui context)
except Exception -> except (ValueError, TypeError, AttributeError)
9. src/shell_runner.py:99 (subprocess cleanup on error)
except Exception -> except (OSError, subprocess.SubprocessError)
10. src/summarize.py:187 (summarise_file fallback)
except Exception -> except (OSError, ValueError, TypeError, AttributeError)
11. src/summarize.py:191 (summarise_file outer)
except Exception -> except (OSError, ValueError, TypeError)
Decisions:
- src/api_hook_client.py: 0 violations; 2 compliant sites; no migration
- src/hot_reloader.py:58 - kept except Exception (module reload can
raise any exception; test fixture uses generic Exception)
- src/api_hooks.py:938-941 - RETHROW (keep as-is; cascading if changed)
Tests verified:
- tests/test_outline_tool.py (3 tests) PASS
- tests/test_hot_reloader.py (8 tests) PASS
- tests/test_hot_reload_integration.py (13 tests) PASS
130 lines
4.1 KiB
Python
130 lines
4.1 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
|
|
|
|
|
|
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 (ValueError, TypeError):
|
|
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 (ValueError, TypeError, AttributeError):
|
|
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." |