# Copyright 2026 Boring for Gemini Authors
# SPDX-License-Identifier: Apache-2.0
"""
Python Language Handler.
Implements analysis using Python's AST module.
"""
import ast
from ..analysis import (
CodeClass,
CodeFunction,
CodeIssue,
DocItem,
DocResult,
ReviewResult,
TestGenResult,
)
from .base import BaseHandler
class PythonHandler(BaseHandler):
"""Handler for Python files using AST."""
@property
def supported_extensions(self) -> list[str]:
return [".py"]
@property
def language_name(self) -> str:
return "Python"
def analyze_for_test_gen(self, file_path: str, source_code: str) -> TestGenResult:
"""Extract functions and classes using AST."""
try:
tree = ast.parse(source_code)
except SyntaxError:
return TestGenResult(file_path=file_path, functions=[], classes=[])
functions = []
classes = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
if not node.name.startswith("_"):
functions.append(
CodeFunction(
name=node.name,
args=[arg.arg for arg in node.args.args if arg.arg != "self"],
docstring=ast.get_docstring(node),
lineno=node.lineno,
is_async=isinstance(node, ast.AsyncFunctionDef),
)
)
elif isinstance(node, ast.ClassDef):
methods = []
for item in node.body:
if isinstance(item, ast.FunctionDef) and not item.name.startswith("_"):
methods.append(item.name)
classes.append(
CodeClass(
name=node.name,
methods=methods,
docstring=ast.get_docstring(node),
lineno=node.lineno,
)
)
return TestGenResult(
file_path=file_path,
functions=functions,
classes=classes,
module_name=file_path.split("/")[-1].replace(".py", ""),
source_language="python",
)
def perform_code_review(
self, file_path: str, source_code: str, focus: str = "all"
) -> ReviewResult:
"""Perform AST-based code review."""
issues = []
# 1. Naming
if focus in ("all", "naming"):
issues.extend(self._check_naming(source_code))
# 2. Error Handling
if focus in ("all", "error_handling"):
issues.extend(self._check_error_handling(source_code))
# 3. Performance
if focus in ("all", "performance"):
issues.extend(self._check_performance(source_code))
# 4. Security
if focus in ("all", "security"):
issues.extend(self._check_security(source_code))
return ReviewResult(file_path=file_path, issues=issues)
def generate_test_code(self, result: TestGenResult, project_root: str) -> str:
"""Generate pytest code."""
# Calculate import path
# Simple heuristic: assume file path is relative to project root or src
import_path = result.file_path.replace("\\", "/").replace(".py", "").replace("/", ".")
if import_path.startswith("src."):
import_path = import_path[4:]
lines = [
"# Auto-generated by Vibe Coder Pro (boring_test_gen)",
"# Run: pytest <this_file>",
"",
"import pytest",
f"from {import_path} import *",
"",
"",
]
for func in result.functions:
lines.append(f"class Test{func.name.title().replace('_', '')}:")
lines.append(f' """Tests for {func.name}."""')
lines.append("")
lines.append(f" def test_{func.name}_basic(self):")
if func.is_async:
lines.append(" # TODO: Implement async test")
lines.append(" # res = await func(...)")
else:
lines.append(f" # TODO: Implement test for {func.name}")
lines.append(" pass")
lines.append("")
for cls in result.classes:
lines.append(f"class Test{cls.name}:")
lines.append(f' """Tests for {cls.name} class."""')
lines.append("")
lines.append(" @pytest.fixture")
lines.append(" def instance(self):")
lines.append(f" return {cls.name}()")
lines.append("")
for method in cls.methods:
lines.append(f" def test_{method}(self, instance):")
lines.append(f" # instance.{method}(...)")
lines.append(" pass")
lines.append("")
return "\n".join(lines)
def extract_dependencies(self, file_path: str, source_code: str) -> list[str]:
"""Extract imports using AST."""
try:
tree = ast.parse(source_code)
except SyntaxError:
return []
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for name in node.names:
imports.append(name.name)
elif isinstance(node, ast.ImportFrom):
module_base = ("." * node.level) + (node.module or "")
if not node.module and node.level > 0:
# Case: from . import utils
# We want to capture '.utils'
for name in node.names:
imports.append(f"{module_base}{name.name}")
else:
# Case: from os import path -> os
# Case: from .utils import func -> .utils
if module_base:
imports.append(module_base)
return sorted(set(imports))
def extract_documentation(self, file_path: str, source_code: str) -> DocResult:
"""Extract documentation using AST."""
try:
tree = ast.parse(source_code)
except SyntaxError:
return DocResult(file_path=file_path, module_doc="", items=[])
module_doc = ast.get_docstring(tree) or ""
items = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
doc = ast.get_docstring(node)
if doc:
items.append(
DocItem(
name=node.name,
type="function",
docstring=doc,
signature=f"def {node.name}(...)",
line_number=node.lineno,
)
)
elif isinstance(node, ast.ClassDef):
doc = ast.get_docstring(node)
if doc:
items.append(
DocItem(
name=node.name,
type="class",
docstring=doc,
signature=f"class {node.name}",
line_number=node.lineno,
)
)
# Check methods
for child in node.body:
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
method_doc = ast.get_docstring(child)
if method_doc:
items.append(
DocItem(
name=f"{node.name}.{child.name}",
type="method",
docstring=method_doc,
signature=f"def {child.name}(...)",
line_number=child.lineno,
)
)
return DocResult(file_path=file_path, module_doc=module_doc, items=items)
# --- Internal Check Methods ---
def _check_naming(self, source: str) -> list[CodeIssue]:
issues = []
try:
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
if any(c.isupper() for c in node.name) and not node.name.isupper():
issues.append(
CodeIssue(
category="Naming",
severity="low",
message=f"Function `{node.name}` uses camelCase",
suggestion="Use snake_case for Python functions",
line=node.lineno,
)
)
except SyntaxError:
pass
return issues
def _check_error_handling(self, source: str) -> list[CodeIssue]:
issues = []
try:
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, ast.ExceptHandler):
if node.type is None:
issues.append(
CodeIssue(
category="Error Handling",
severity="high",
message="Bare `except:` used",
suggestion="Specify exception type (e.g. `except ValueError:`)",
line=node.lineno,
)
)
except SyntaxError:
pass
return issues
def _check_performance(self, source: str) -> list[CodeIssue]:
issues = []
lines = source.split("\n")
# AST based check could be better for loops, but line-based is faster for simple patterns
in_loop = False
loop_indent = 0
for i, line in enumerate(lines, 1):
stripped = line.strip()
indent = len(line) - len(line.lstrip())
# Simple loop detection (naive)
if stripped.startswith("for ") or stripped.startswith("while "):
in_loop = True
loop_indent = indent
elif indent <= loop_indent and stripped:
in_loop = False
if in_loop:
if ".append(" in line:
issues.append(
CodeIssue(
category="Performance",
severity="low",
message="Using `.append()` inside loop",
suggestion="Consider list comprehension for better performance",
line=i,
)
)
if ".query(" in line or ".objects.get(" in line or ".filter(" in line:
issues.append(
CodeIssue(
category="Performance",
severity="high",
message="Potential N+1 Query",
suggestion="Use `select_related` or `prefetch_related` outside loop",
line=i,
)
)
if "open(" in line:
issues.append(
CodeIssue(
category="Performance",
severity="medium",
message="File I/O inside loop",
suggestion="Move file opening strictly outside the loop",
line=i,
)
)
if "time.sleep(" in line:
issues.append(
CodeIssue(
category="Performance",
severity="medium",
message="Blocking sleep inside loop",
suggestion="Consider async/await or event-driven approach",
line=i,
)
)
return issues
def _check_security(self, source: str) -> list[CodeIssue]:
issues = []
dangerous = [
("eval(", "high", "Avoid `eval()` (code injection risk)"),
("exec(", "high", "Avoid `exec()` (code injection risk)"),
("shell=True", "medium", "Avoid `shell=True` in subprocess"),
("password =", "medium", "Possible hardcoded password"),
("api_key =", "medium", "Possible hardcoded API key"),
]
lines = source.split("\n")
for i, line in enumerate(lines, 1):
for pat, sev, msg in dangerous:
if pat in line:
issues.append(CodeIssue(category="Security", severity=sev, message=msg, line=i))
return issues