scripts/check_deprecated_apis.py
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""Fail CI if deprecated datetime/asyncio call patterns return.
The guard parses tracked Python source with the stdlib AST so references inside
comments, docs, and string-based regression tests do not create false positives.
Historical code under archived/ is intentionally excluded.
"""
from __future__ import annotations
import ast
import subprocess
import sys
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True, order=True)
class Finding:
path: Path
line: int
column: int
api: str
replacement: str
_BANNED = {
"datetime.utcnow": "use timezone-aware datetime.now(UTC)",
"asyncio.get_event_loop": "use get_running_loop() in async code or an explicit loop lifecycle",
}
def _call_name(node: ast.AST) -> str:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
prefix = _call_name(node.value)
return f"{prefix}.{node.attr}" if prefix else node.attr
return ""
def scan_file(path: Path) -> list[Finding]:
source = path.read_text(encoding="utf-8-sig")
tree = ast.parse(source, filename=str(path))
findings: list[Finding] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
api = _call_name(node.func)
replacement = _BANNED.get(api)
if replacement is None:
continue
findings.append(
Finding(
path=path,
line=node.lineno,
column=node.col_offset + 1,
api=api,
replacement=replacement,
)
)
return sorted(findings)
def _is_archived(path: Path, root: Path) -> bool:
try:
relative = path.resolve().relative_to(root.resolve())
except ValueError:
return False
return bool(relative.parts and relative.parts[0].lower() == "archived")
def scan_paths(paths: Iterable[Path], *, root: Path) -> list[Finding]:
findings: list[Finding] = []
for path in paths:
if _is_archived(path, root):
continue
findings.extend(scan_file(path))
return sorted(findings)
def tracked_python_files(root: Path) -> list[Path]:
result = subprocess.run(
["git", "ls-files", "-z", "*.py"],
cwd=root,
check=True,
capture_output=True,
)
return [root / item.decode("utf-8") for item in result.stdout.split(b"\0") if item]
def main() -> int:
root = Path(__file__).resolve().parents[1]
try:
findings = scan_paths(tracked_python_files(root), root=root)
except (OSError, SyntaxError, subprocess.CalledProcessError) as exc:
print(f"Deprecated-API guard failed closed: {exc}", file=sys.stderr)
return 2
if findings:
print("Deprecated API calls are not allowed outside archived/:", file=sys.stderr)
for finding in findings:
try:
display = finding.path.relative_to(root)
except ValueError:
display = finding.path
print(
f" {display}:{finding.line}:{finding.column}: {finding.api}() - {finding.replacement}",
file=sys.stderr,
)
return 1
print(
"OK: no banned datetime.utcnow() or asyncio.get_event_loop() calls in tracked current Python."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())