Ctrl K
ComunidadGuías…

scripts/check_deprecated_apis.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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())