← Files ClaraARCHIVED FILE

modules/statement-analysis/scripts/check_dependencies.py

4.11 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

"""Dependency checker for the statement-analysis plugin."""

from __future__ import annotations

import argparse
import importlib
import importlib.util
import json
import re
import sys
import warnings
from pathlib import Path

__all__ = ["main"]

REQUIRED_MODULES = ("csv", "json", "pathlib")


def plugin_root() -> Path:
    """Return the plugin root directory."""

    return Path(__file__).resolve().parents[1]


def requirement_name(line: str) -> str:
    """Return the importable package token from one requirements line."""

    cleaned = line.split("#", 1)[0].strip()
    if not cleaned or cleaned.startswith(("-", "git+", "http://", "https://")):
        return ""
    match = re.match(r"([A-Za-z0-9_.-]+)", cleaned)
    return match.group(1).lower() if match else ""


def import_name(package_name: str) -> str:
    """Translate a package name to its normal import name."""

    return package_name.replace("-", "_")


def modules_from_requirements(requirement_files: list[Path]) -> list[str]:
    """Return import names declared by explicit requirements files."""

    modules: list[str] = []
    for requirements_file in requirement_files:
        for line in requirements_file.read_text(encoding="utf-8").splitlines():
            package = requirement_name(line)
            if package:
                modules.append(import_name(package))
    return modules


def selected_requirement_files(explicit_files: list[str]) -> list[Path]:
    """Resolve requested requirements files under the plugin root."""

    root = plugin_root()
    if explicit_files:
        files = [root / name for name in explicit_files]
    else:
        files = [root / "requirements.txt"]
    return [path for path in files if path.exists()]


def collect_import_warnings(module_name: str) -> list[str]:
    """Import one module and return warning messages raised during import."""

    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        importlib.import_module(module_name)
    return [f"{warning.category.__name__}: {warning.message}" for warning in caught]


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    """Parse command-line arguments."""

    parser = argparse.ArgumentParser(
        description="Check runtime dependencies for the statement-analysis plugin."
    )
    parser.add_argument("--output", type=Path, default=None)
    parser.add_argument(
        "--requirements",
        action="append",
        default=[],
        help="Specific requirements file under the plugin root. May be passed more than once.",
    )
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    """Check runtime imports and optionally write a JSON diagnostic."""

    args = parse_args(argv or sys.argv[1:])
    requirement_files = selected_requirement_files(args.requirements)
    if args.requirements and not requirement_files:
        sys.stdout.write("MISSING_REQUIREMENTS_FILE: no requested requirements file found\n")
        return 1
    required_modules = (
        tuple(modules_from_requirements(requirement_files))
        if args.requirements
        else REQUIRED_MODULES
    )

    missing: list[str] = []
    import_warnings: dict[str, list[str]] = {}
    for module_name in required_modules:
        if importlib.util.find_spec(module_name) is None:
            missing.append(f"{module_name}: module not found")
            continue
        try:
            import_warnings[module_name] = collect_import_warnings(module_name)
        except (ImportError, RuntimeError, ValueError) as exc:
            missing.append(f"{module_name}: {type(exc).__name__}: {exc}")

    payload = {
        "status": "ok" if not missing else "missing_dependencies",
        "required_modules": list(required_modules),
        "missing": missing,
        "warnings": import_warnings,
    }
    if args.output is not None:
        args.output.parent.mkdir(parents=True, exist_ok=True)
        args.output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
    sys.stdout.write(json.dumps(payload, indent=2) + "\n")
    return 0 if not missing else 1


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: b8e5bf73fe98264fa7d48718ffd33137e4997a7da7fbdd915d30e6b71ffe5550