76 lines
2.6 KiB
Python
76 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Require docstrings on public production APIs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
|
|
def _needs_docstring(node: ast.AST, *, parent_class: str | None = None) -> bool:
|
|
"""Return whether `node` should carry an API contract docstring."""
|
|
|
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
name = node.name
|
|
if name.startswith("_") and name != "__init__":
|
|
return False
|
|
return not (parent_class and name.startswith("_"))
|
|
if isinstance(node, ast.ClassDef):
|
|
if node.name.startswith("_"):
|
|
return False
|
|
if any(
|
|
(isinstance(dec, ast.Name) and dec.id == "dataclass")
|
|
or (isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name) and dec.func.id == "dataclass")
|
|
for dec in node.decorator_list
|
|
):
|
|
return False
|
|
if any(
|
|
isinstance(base, ast.Name) and base.id in {"Exception", "RuntimeError", "BaseException"}
|
|
for base in node.bases
|
|
):
|
|
return False
|
|
return not any(isinstance(base, ast.Name) and base.id == "BaseModel" for base in node.bases)
|
|
return False
|
|
|
|
|
|
def _iter_nodes(tree: ast.AST) -> list[tuple[ast.AST, str | None]]:
|
|
"""Yield top-level surface area nodes for contract checking."""
|
|
|
|
return [(node, None) for node in getattr(tree, "body", [])]
|
|
|
|
|
|
def main() -> int:
|
|
"""Scan the production package and fail on missing docstrings."""
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", default="ariadne")
|
|
args = parser.parse_args()
|
|
|
|
root = Path(args.root)
|
|
violations: list[str] = []
|
|
for path in sorted(root.rglob("*.py")):
|
|
if "__pycache__" in path.parts or ".venv" in path.parts:
|
|
continue
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
for node, parent_class in _iter_nodes(tree):
|
|
if not _needs_docstring(node, parent_class=parent_class):
|
|
continue
|
|
if ast.get_docstring(node):
|
|
continue
|
|
if isinstance(node, ast.ClassDef):
|
|
violations.append(f"{path}: class {node.name} is missing a docstring")
|
|
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
owner = f"{parent_class}." if parent_class else ""
|
|
violations.append(f"{path}: {owner}{node.name} is missing a docstring")
|
|
|
|
if violations:
|
|
for item in violations:
|
|
print(item)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|