.pydev-tools / _ast / docstrings.py
.pydev-tools / _ast / docstrings.py
#!/usr/bin/env python3
"""Audit docstring coverage across a source tree.
Reports per-file total vs covered docstrings (functions/classes/modules) plus
overall percentages, and lists missing targets. Emits JSON.
Usage: docstrings.py <target_path> [output_dir]
target_path : file or directory to scan (default: current dir)
output_dir : optional dir to write a report .md into
"""
import ast, json, os, sys
def load_py_files(path):
files = []
if os.path.isfile(path):
files.append(path)
elif os.path.isdir(path):
for root, _dirs, names in os.walk(path):
for fn in sorted(names):
if fn.endswith(".py"):
files.append(os.path.join(root, fn))
return sorted(files)
def count_docstrings(tree):
total = 0
covered = 0
for node in ast.walk(tree):
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
total += 1
if ast.get_docstring(node) is not None:
covered += 1
return total, covered
def main():
files = load_py_files(sys.argv[1])
per_file = []
grand_total = 0
grand_covered = 0
missing = []
for f in files:
try:
tree = ast.parse(open(f, encoding="utf-8").read())
except (SyntaxError, OSError):
continue
total, covered = count_docstrings(tree)
pct = round(100.0 * covered / total, 1) if total else 100.0
grand_total += total
grand_covered += covered
if total > 0 and covered < total:
missing.append({"path": f, "total": total, "covered": covered})
per_file.append({"path": f, "total": total, "covered": covered, "pct": pct})
overall = round(100.0 * grand_covered / grand_total, 1) if grand_total else 100.0
out = {
"overallPct": overall,
"grandTotal": grand_total,
"grandCovered": grand_covered,
"perFile": per_file,
"missingFiles": missing,
"reportPath": None,
}
out_dir = sys.argv[2].strip() if len(sys.argv) > 2 else "."
md_path = os.path.join(out_dir, "docstring_report.md")
lines = ["# Docstring Coverage Report", "", f"Overall: {overall}% ({grand_covered}/{grand_total} documented)", ""]
for item in per_file:
lines.append(f"## `{item['path']}` — {item['pct']}%")
lines.append("")
lines.append("")
lines.append("## Missing docstrings (per file)")
lines.append("")
if missing:
for m in missing:
lines.append(f"- `{m['path']}`: {m['covered']}/{m['total']} documented")
else:
lines.append("- All files fully covered.")
try:
with open(md_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines))
out["reportPath"] = md_path
except OSError:
pass
json.dump(out, sys.stdout, indent=2)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Audit docstring coverage across a source tree.
Reports per-file total vs covered docstrings (functions/classes/modules) plus
overall percentages, and lists missing targets. Emits JSON.
Usage: docstrings.py <target_path> [output_dir]
target_path : file or directory to scan (default: current dir)
output_dir : optional dir to write a report .md into
"""
import ast, json, os, sys
def load_py_files(path):
files = []
if os.path.isfile(path):
files.append(path)
elif os.path.isdir(path):
for root, _dirs, names in os.walk(path):
for fn in sorted(names):
if fn.endswith(".py"):
files.append(os.path.join(root, fn))
return sorted(files)
def count_docstrings(tree):
total = 0
covered = 0
for node in ast.walk(tree):
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
total += 1
if ast.get_docstring(node) is not None:
covered += 1
return total, covered
def main():
files = load_py_files(sys.argv[1])
per_file = []
grand_total = 0
grand_covered = 0
missing = []
for f in files:
try:
tree = ast.parse(open(f, encoding="utf-8").read())
except (SyntaxError, OSError):
continue
total, covered = count_docstrings(tree)
pct = round(100.0 * covered / total, 1) if total else 100.0
grand_total += total
grand_covered += covered
if total > 0 and covered < total:
missing.append({"path": f, "total": total, "covered": covered})
per_file.append({"path": f, "total": total, "covered": covered, "pct": pct})
overall = round(100.0 * grand_covered / grand_total, 1) if grand_total else 100.0
out = {
"overallPct": overall,
"grandTotal": grand_total,
"grandCovered": grand_covered,
"perFile": per_file,
"missingFiles": missing,
"reportPath": None,
}
out_dir = sys.argv[2].strip() if len(sys.argv) > 2 else "."
md_path = os.path.join(out_dir, "docstring_report.md")
lines = ["# Docstring Coverage Report", "", f"Overall: {overall}% ({grand_covered}/{grand_total} documented)", ""]
for item in per_file:
lines.append(f"## `{item['path']}` — {item['pct']}%")
lines.append("")
lines.append("")
lines.append("## Missing docstrings (per file)")
lines.append("")
if missing:
for m in missing:
lines.append(f"- `{m['path']}`: {m['covered']}/{m['total']} documented")
else:
lines.append("- All files fully covered.")
try:
with open(md_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines))
out["reportPath"] = md_path
except OSError:
pass
json.dump(out, sys.stdout, indent=2)
if __name__ == "__main__":
main()