"""Scan all .py files for missing type hints. Writes scan_report.txt.""" import ast, os SKIP: set[str] = {'.git', '__pycache__', '.venv', 'venv', 'node_modules', '.claude', '.gemini'} BASE: str = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) os.chdir(BASE) results: dict[str, tuple[int, int, int, int]] = {} for root, dirs, files in os.walk('.'): dirs[:] = [d for d in dirs if d not in SKIP] for f in files: if not f.endswith('.py'): continue path: str = os.path.join(root, f).replace('\\', '/') try: with open(path, 'r', encoding='utf-8-sig') as fh: tree = ast.parse(fh.read()) except Exception: continue counts: list[int] = [0, 0, 0] # nr, up, uv def scan(scope: ast.AST, prefix: str = '') -> None: # Iterate top-level nodes in this scope for node in ast.iter_child_nodes(scope): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): if node.returns is None: counts[0] += 1 for arg in node.args.args: if arg.arg not in ('self', 'cls') and arg.annotation is None: counts[1] += 1 elif isinstance(node, ast.Assign): for t in node.targets: if isinstance(t, ast.Name): counts[2] += 1 elif isinstance(node, ast.ClassDef): scan(node, prefix=f'{node.name}.') scan(tree) nr, up, uv = counts total: int = nr + up + uv if total > 0: results[path] = (nr, up, uv, total) lines: list[str] = [] lines.append(f'Files with untyped items: {len(results)}') lines.append('') lines.append(f'{"File":<58} {"NoRet":>6} {"Params":>7} {"Vars":>5} {"Total":>6}') lines.append('-' * 85) gt: int = 0 for path in sorted(results, key=lambda x: results[x][3], reverse=True): nr, up, uv, t = results[path] lines.append(f'{path:<58} {nr:>6} {up:>7} {uv:>5} {t:>6}') gt += t lines.append('-' * 85) lines.append(f'{"TOTAL":<58} {"":>6} {"":>7} {"":>5} {gt:>6}') report: str = '\n'.join(lines) with open('scan_report.txt', 'w', encoding='utf-8') as f: f.write(report)