#!/usr/bin/env python3 """Fix S112 (try-except-continue) by logging the exception before continuing.""" from __future__ import annotations import json import re import subprocess import sys from pathlib import Path LOG_LINE = 'logging.getLogger(__name__).warning("swallowed exception", exc_info=True)' SAME_LINE_CONTINUE = re.compile( r"^(?P[ \t]*)except\b(?P[^:\n]*):\s*continue\s*(?:#.*)?$", re.MULTILINE, ) def ensure_import(text: str, module: str) -> str: if re.search(rf"(?:^|\n)import\s+{re.escape(module)}(?:\s|$)", text): return text if re.search(rf"(?:^|\n)from\s+{re.escape(module)}\s+import", text): return text lines = text.splitlines(keepends=True) insert = 0 paren = 0 for i, ln in enumerate(lines): paren += ln.count("(") - ln.count(")") if ln.startswith("#!/") or ln.startswith("# -*-") or ln.startswith("# coding"): insert = i + 1 continue if ln.startswith("from __future__ import"): insert = i + 1 continue if paren == 0 and (ln.startswith("import ") or ln.startswith("from ")): insert = i + 1 if insert == len(lines) and lines and not lines[-1].endswith("\n"): lines.append("\n") lines.insert(insert, f"import {module}\n") return "".join(lines) def fix_file(path: Path) -> int: text = path.read_text() orig = text # same-line: except X: continue text = SAME_LINE_CONTINUE.sub( lambda m: ( f"{m.group('indent')}except {m.group('expr').strip()}:\n" f"{m.group('indent')} {LOG_LINE}\n" f"{m.group('indent')} continue" ), text, ) if text != orig: text = ensure_import(text, "logging") path.write_text(text) return 1 return 0 if __name__ == "__main__": proc = subprocess.run( [ sys.executable, "-m", "ruff", "check", "--select", "S112", "--output-format", "json", "app/", ], capture_output=True, text=True, ) errors = json.loads(proc.stdout or "[]") by_file: dict[str, list[int]] = {} for e in errors: by_file.setdefault(e["filename"], []).append(e["location"]["row"]) total_files = 0 for fn, rows in by_file.items(): path = Path(fn) text = path.read_text() orig = text text = ensure_import(text, "logging") lines = text.splitlines(keepends=True) for row in sorted(rows, reverse=True): if row < 1 or row > len(lines): continue idx = row - 1 line = lines[idx] m = SAME_LINE_CONTINUE.match(line) if m: indent = m.group("indent") expr = m.group("expr").strip() new_lines = [ f"{indent}except {expr}:\n", f"{indent} {LOG_LINE}\n", f"{indent} continue\n", ] lines[idx : idx + 1] = new_lines continue # multiline: find next continue j = idx + 1 while j < len(lines): content = lines[j].strip() if content == "" or content.startswith("#"): j += 1 continue if ( content == "continue" or content.startswith("continue ") or content.startswith("continue\t") ): indent = len(lines[j]) - len(lines[j].lstrip()) lines[j] = " " * indent + LOG_LINE + "\n" + " " * indent + "continue\n" break break text = "".join(lines) if text != orig: path.write_text(text) total_files += 1 print(f"S112: fixed {len(errors)} errors in {total_files} files")