-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathaudit_docstrings.py
More file actions
138 lines (110 loc) · 3.96 KB
/
audit_docstrings.py
File metadata and controls
138 lines (110 loc) · 3.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/usr/bin/env python3
"""Script to audit docstring coverage in source files."""
from __future__ import annotations
import ast
from pathlib import Path
from typing import Any
def audit_file(file_path: Path) -> dict[str, Any]:
"""Audit a Python file for missing docstrings.
Args:
file_path: Path to Python file to audit
Returns:
Dictionary with audit results
"""
results = {
"file": str(file_path),
"functions": [],
"classes": [],
"missing_docstrings": [],
}
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
tree = ast.parse(content, filename=str(file_path))
except Exception as e:
results["error"] = str(e)
return results
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
# Skip private methods (starting with _) unless they're __init__ or __str__ etc
if node.name.startswith("_") and not node.name.startswith("__"):
continue
has_docstring = ast.get_docstring(node) is not None or (
node.body
and isinstance(node.body[0], ast.Expr)
and isinstance(node.body[0].value, ast.Constant)
and isinstance(node.body[0].value.value, str)
)
results["functions"].append(
{
"name": node.name,
"line": node.lineno,
"has_docstring": has_docstring,
}
)
if not has_docstring:
results["missing_docstrings"].append(
{
"type": "function",
"name": node.name,
"line": node.lineno,
}
)
elif isinstance(node, ast.ClassDef):
has_docstring = ast.get_docstring(node) is not None
results["classes"].append(
{
"name": node.name,
"line": node.lineno,
"has_docstring": has_docstring,
}
)
if not has_docstring:
results["missing_docstrings"].append(
{
"type": "class",
"name": node.name,
"line": node.lineno,
}
)
return results
def main():
"""Main audit function."""
src_dir = Path("src/metainformant")
all_results = []
total_missing = 0
for py_file in src_dir.rglob("*.py"):
if py_file.name.startswith("__") and py_file.name != "__init__.py":
continue
results = audit_file(py_file)
if "error" not in results:
missing_count = len(results["missing_docstrings"])
if missing_count > 0:
all_results.append(results)
total_missing += missing_count
# Print summary
print(f"Found {total_missing} missing docstrings across {len(all_results)} files\n")
for result in sorted(all_results, key=lambda x: len(x["missing_docstrings"]), reverse=True):
if result["missing_docstrings"]:
print(f"\n{result['file']}:")
for item in result["missing_docstrings"]:
print(f" {item['type']}: {item['name']} (line {item['line']})")
# Write detailed report
report_path = Path("output/audit_docstring_report.json")
report_path.parent.mkdir(parents=True, exist_ok=True)
import json
with open(report_path, "w") as f:
json.dump(
{
"summary": {
"total_files_with_missing": len(all_results),
"total_missing_docstrings": total_missing,
},
"details": all_results,
},
f,
indent=2,
)
print(f"\n\nDetailed report written to {report_path}")
if __name__ == "__main__":
main()