scripts/interface_order.py (104 lines of code) (raw):

#!/usr/bin/env python3 import ast import enum import pathlib import sys from collections.abc import Sequence from typing import Final _CLASS_EXEMPTIONS: Final[set[str]] = { "atr/datasources/apache.py", "atr/db/models.py", } class ExitCode(enum.IntEnum): SUCCESS = 0 FAILURE = 1 USAGE_ERROR = 2 def check_order(file_path: pathlib.Path, quiet: bool) -> bool: content = _read_file_content(file_path) if content is None: sys.exit(ExitCode.FAILURE) tree = _parse_python_code(content, str(file_path)) if tree is None: sys.exit(ExitCode.FAILURE) class_names = _extract_top_level_class_names(tree) function_names = _extract_top_level_function_names(tree) all_ok = True if not _verify_names_are_sorted(function_names, str(file_path), "function"): all_ok = False for class_name in class_names: if class_name.startswith("_"): print(f"!! {file_path} - class '{class_name}' is private", file=sys.stderr) all_ok = False if (not quiet) or (str(file_path) not in _CLASS_EXEMPTIONS): if not _verify_names_are_sorted(class_names, str(file_path), "class"): all_ok = False return all_ok def main() -> None: quiet = sys.argv[2:3] == ["--quiet"] argc = len(sys.argv) match (argc, quiet): case (2, False): ... case (3, True): ... case _: print("Usage: python interface_order.py <filename> [ --quiet ]", file=sys.stderr) sys.exit(ExitCode.USAGE_ERROR) file_path = pathlib.Path(sys.argv[1]) all_ok = check_order(file_path, quiet) if all_ok: if not quiet: print(f"ok {file_path}") sys.exit(ExitCode.SUCCESS) else: sys.exit(ExitCode.FAILURE) def _extract_top_level_function_names(tree: ast.Module) -> list[str]: function_names: list[str] = [] for node in tree.body: if isinstance(node, ast.AsyncFunctionDef) or isinstance(node, ast.FunctionDef): function_names.append(_toggle_sortability(node.name)) return function_names def _extract_top_level_class_names(tree: ast.Module) -> list[str]: class_names: list[str] = [] for node in tree.body: if isinstance(node, ast.ClassDef): class_names.append(node.name) return class_names def _parse_python_code(code: str, filename: str) -> ast.Module | None: try: return ast.parse(code, filename=filename) except SyntaxError as e: print(f"Error: Invalid Python syntax in {filename}: {e}", file=sys.stderr) return None def _read_file_content(file_path: pathlib.Path) -> str | None: try: return file_path.read_text(encoding="utf-8") except FileNotFoundError: print(f"Error: File not found: {file_path}", file=sys.stderr) return None except OSError as e: print(f"Error: Could not read file {file_path}: {e}", file=sys.stderr) return None def _toggle_sortability(name: str) -> str: if name.startswith("_"): return name[1:] else: return "_" + name def _verify_names_are_sorted(names: Sequence[str], filename: str, interface_type: str) -> bool: is_sorted = all(names[i] <= names[i + 1] for i in range(len(names) - 1)) if is_sorted: return True for i in range(len(names) - 1): if names[i] > names[i + 1]: if interface_type == "class": a = names[i] b = names[i + 1] else: a = _toggle_sortability(names[i]) b = _toggle_sortability(names[i + 1]) print( f"!! {filename} - {interface_type} '{b}' is misordered relative to '{a}'", file=sys.stderr, ) return False if __name__ == "__main__": main()