9e8f1bbeed
Lint PowerShell / powershell-lint (windows-latest) (push) Waiting to run
Matrix Smoke / macos-smoke (push) Waiting to run
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
274 lines
8.9 KiB
Python
274 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Docstring Generator using Claude Haiku 4.5
|
|
|
|
Generates Google-style docstrings for functions missing documentation.
|
|
Processes in batches of 8 functions per API call for cost efficiency.
|
|
|
|
Cost: $20-35 per run (depending on function count and complexity)
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from anthropic_helper import create_message
|
|
|
|
|
|
def load_missing_docs(docs_path: Path) -> list[dict[str, Any]]:
|
|
"""Load functions without docstrings from JSON."""
|
|
try:
|
|
with open(docs_path, "r") as f:
|
|
docs = json.load(f)
|
|
return docs
|
|
except FileNotFoundError:
|
|
print(f"::error::Docs file not found: {docs_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except json.JSONDecodeError:
|
|
print(f"::error::Invalid JSON in docs file: {docs_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def read_function_code(file_path: str, function_name: str, line: int) -> str:
|
|
"""Read the function code from file."""
|
|
try:
|
|
with open(file_path, "r") as f:
|
|
lines = f.readlines()
|
|
|
|
start_line = line - 1
|
|
code_lines = []
|
|
indent_level = None
|
|
|
|
for i in range(start_line, len(lines)):
|
|
line_text = lines[i]
|
|
|
|
if indent_level is None:
|
|
indent_level = len(line_text) - len(line_text.lstrip())
|
|
|
|
current_indent = len(line_text) - len(line_text.lstrip())
|
|
if i > start_line and current_indent <= indent_level and line_text.strip():
|
|
if line_text.strip().startswith(("def ", "class ", "@")):
|
|
break
|
|
|
|
code_lines.append(line_text)
|
|
|
|
if len(code_lines) >= 50:
|
|
break
|
|
|
|
return "".join(code_lines)
|
|
except Exception as e:
|
|
return f"# Could not read function code: {e}"
|
|
|
|
|
|
def build_docstring_prompt(functions: list[dict[str, Any]]) -> str:
|
|
"""Build the prompt for generating docstrings."""
|
|
functions_formatted = []
|
|
|
|
for i, func in enumerate(functions, 1):
|
|
code = read_function_code(func["file"], func["function"], func["line"])
|
|
functions_formatted.append(f"""
|
|
### Function {i}: `{func["function"]}` in `{func["file"]}`
|
|
|
|
```python
|
|
{code}
|
|
```
|
|
""")
|
|
|
|
functions_text = "\n".join(functions_formatted)
|
|
|
|
return f"""You are a Python documentation expert. Generate clear, helpful Google-style docstrings for functions missing documentation.
|
|
|
|
## Functions to Document ({len(functions)} total)
|
|
|
|
{functions_text}
|
|
|
|
## Your Task
|
|
|
|
For each function, provide a complete Google-style docstring including:
|
|
1. **Summary line** - One sentence describing what the function does
|
|
2. **Args section** - Document each parameter with type and description
|
|
3. **Returns section** - Document return value with type and description
|
|
4. **Raises section** - Document exceptions raised (if applicable)
|
|
5. **Examples section** (optional) - Usage examples for complex functions
|
|
|
|
## Guidelines
|
|
|
|
- Summary line: Start with imperative verb (e.g., "Calculate", "Return", "Process")
|
|
- Be concise but informative
|
|
- Don't repeat the function name in the summary
|
|
- Use present tense for descriptions
|
|
- Include type information in Args/Returns even if type hints exist
|
|
- Only include Raises section if function actually raises exceptions
|
|
- Only include Example section for non-trivial functions
|
|
|
|
## Output Format
|
|
|
|
Respond with JSON:
|
|
|
|
```json
|
|
{{
|
|
"functions_documented": [
|
|
{{
|
|
"file": "app/example.py",
|
|
"function": "process_data",
|
|
"line": 42,
|
|
"docstring": "Process data items and return results.\\n\\nArgs:\\n data: List of items to process.\\n options: Optional configuration dict.\\n\\nReturns:\\n Processed results as dict."
|
|
}}
|
|
],
|
|
"summary": {{
|
|
"total_documented": 5,
|
|
"functions_with_examples": 2,
|
|
"functions_with_raises": 3
|
|
}}
|
|
}}
|
|
```
|
|
|
|
Begin your analysis."""
|
|
|
|
|
|
def generate_docstrings_batch(
|
|
functions: list[dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
"""Generate docstrings for a batch of functions using Claude Haiku 4.5."""
|
|
prompt = build_docstring_prompt(functions)
|
|
|
|
try:
|
|
response = create_message(
|
|
model="claude-haiku-4-5-20251001",
|
|
max_tokens=4096,
|
|
temperature=0,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
)
|
|
|
|
response_text = ""
|
|
for block in response.content:
|
|
if block.type == "text":
|
|
response_text += block.text
|
|
|
|
json_start = response_text.find("```json")
|
|
if json_start != -1:
|
|
json_start = response_text.find("\n", json_start) + 1
|
|
json_end = response_text.find("```", json_start)
|
|
response_text = response_text[json_start:json_end].strip()
|
|
else:
|
|
json_start = response_text.find("{")
|
|
json_end = response_text.rfind("}") + 1
|
|
if json_start != -1 and json_end > json_start:
|
|
response_text = response_text[json_start:json_end]
|
|
|
|
result = json.loads(response_text)
|
|
|
|
result["_metadata"] = {
|
|
"model": "claude-haiku-4-5-20251001",
|
|
"input_tokens": response.usage.input_tokens,
|
|
"output_tokens": response.usage.output_tokens,
|
|
"total_tokens": response.usage.input_tokens + response.usage.output_tokens,
|
|
}
|
|
|
|
return result
|
|
|
|
except json.JSONDecodeError:
|
|
print("::error::Failed to parse JSON from Claude response", file=sys.stderr)
|
|
print(f"Response text: {response_text[:500]}", file=sys.stderr)
|
|
return {
|
|
"functions_documented": [],
|
|
"summary": {"total_documented": 0},
|
|
"_metadata": {"error": "JSON parse error"},
|
|
}
|
|
except Exception as e:
|
|
print(f"::error::API error: {e}", file=sys.stderr)
|
|
return {
|
|
"functions_documented": [],
|
|
"summary": {"total_documented": 0},
|
|
"_metadata": {"error": str(e)},
|
|
}
|
|
|
|
|
|
def estimate_cost(input_tokens: int, output_tokens: int) -> float:
|
|
"""Estimate cost based on Claude Haiku 4.5 pricing."""
|
|
input_cost = (input_tokens / 1_000_000) * 1
|
|
output_cost = (output_tokens / 1_000_000) * 5
|
|
return round(input_cost + output_cost, 2)
|
|
|
|
|
|
def main() -> None:
|
|
"""Main entry point for docstring generation."""
|
|
docs_path = Path(sys.argv[1] if len(sys.argv) > 1 else "/tmp/missing-docs.json")
|
|
output_path = Path(
|
|
sys.argv[2] if len(sys.argv) > 2 else "/tmp/documentation-suggestions.json"
|
|
)
|
|
|
|
print(f"Loading functions without docstrings from {docs_path}...")
|
|
functions = load_missing_docs(docs_path)
|
|
|
|
if not functions:
|
|
print("No functions need docstrings. Skipping generation.")
|
|
result = {
|
|
"functions_documented": [],
|
|
"summary": {"total_documented": 0},
|
|
"_metadata": {
|
|
"model": "N/A",
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"total_tokens": 0,
|
|
},
|
|
}
|
|
else:
|
|
print(f"Generating docstrings for {len(functions)} functions...")
|
|
|
|
all_results = []
|
|
total_input_tokens = 0
|
|
total_output_tokens = 0
|
|
|
|
batch_size = 8
|
|
for i in range(0, len(functions), batch_size):
|
|
batch = functions[i : i + batch_size]
|
|
print(f"Processing batch {i // batch_size + 1} ({len(batch)} functions)...")
|
|
|
|
batch_result = generate_docstrings_batch(batch)
|
|
all_results.extend(batch_result.get("functions_documented", []))
|
|
|
|
metadata = batch_result.get("_metadata", {})
|
|
total_input_tokens += metadata.get("input_tokens", 0)
|
|
total_output_tokens += metadata.get("output_tokens", 0)
|
|
|
|
result = {
|
|
"functions_documented": all_results,
|
|
"summary": {"total_documented": len(all_results)},
|
|
"_metadata": {
|
|
"model": "claude-haiku-4-5-20251001",
|
|
"input_tokens": total_input_tokens,
|
|
"output_tokens": total_output_tokens,
|
|
"total_tokens": total_input_tokens + total_output_tokens,
|
|
},
|
|
}
|
|
|
|
with open(output_path, "w") as f:
|
|
json.dump(result, f, indent=2)
|
|
|
|
print(f"Docstring generation complete. Results saved to {output_path}")
|
|
|
|
summary = result.get("summary", {})
|
|
metadata = result.get("_metadata", {})
|
|
|
|
print("\n## Documentation Generation Results\n")
|
|
print(f"- **Functions documented**: {summary.get('total_documented', 0)}")
|
|
|
|
if metadata.get("input_tokens"):
|
|
cost = estimate_cost(metadata["input_tokens"], metadata["output_tokens"])
|
|
print(f"\n**Cost**: ${cost}")
|
|
print(
|
|
f"**Tokens**: {metadata['total_tokens']:,} ({metadata['input_tokens']:,} in + {metadata['output_tokens']:,} out)"
|
|
)
|
|
|
|
github_output = os.environ.get("GITHUB_OUTPUT", "")
|
|
if github_output:
|
|
with open(github_output, "a") as f:
|
|
f.write(f"docstring_cost={cost}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|