Ctrl K
ComunidadGuías…

rex/exception_handler.py

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

Archivo original ↗Preguntar a KiBot
"""Global unhandled exception handler (US-103).

Wraps application entry points so that any unhandled exception is:
  - logged with full context (type, message, traceback, timestamp)
  - shown to the user as a clean "Error: …" message (no raw traceback)
  - followed by a non-zero exit (sys.exit(1))

In debug mode (``REX_DEBUG=1``), the full traceback is also printed to
stderr so developers can see the full call stack.

:class:`SystemExit` and :class:`KeyboardInterrupt` are passed through
unchanged so that normal ``sys.exit(0)`` calls and Ctrl-C work correctly.

Usage::

    from rex.exception_handler import wrap_entrypoint

    @wrap_entrypoint
    def main() -> int:
        ...
"""

from __future__ import annotations

import logging
import sys
import traceback
from collections.abc import Callable
from datetime import UTC, datetime
from functools import wraps
from typing import Any, TypeVar

_LOG = logging.getLogger(__name__)

F = TypeVar("F", bound=Callable[..., Any])


def handle_unhandled_exception(
    exc: BaseException,
    logger: logging.Logger = _LOG,
) -> None:
    """Log *exc* with full context (type, message, traceback, timestamp).

    This function only logs — it does NOT exit.  Call ``sys.exit(1)`` after
    this if you want to terminate the process.  Keeping log-and-exit separate
    makes the function testable.
    """
    tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
    timestamp = datetime.now(UTC).isoformat()
    logger.critical(
        "Unhandled exception — %s: %s | timestamp=%s\n%s",
        type(exc).__name__,
        exc,
        timestamp,
        tb,
    )


def _print_user_error(exc: Exception) -> None:
    """Print a user-friendly error message to stderr.

    In normal mode, only the error message is shown (no traceback).
    In debug mode (``REX_DEBUG=1``), the full traceback is also printed.
    """
    from rex.dep_errors import is_debug_mode

    if is_debug_mode():
        tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
        print(tb, file=sys.stderr, end="")
    else:
        print(f"\nError: {exc}", file=sys.stderr)
        print("Run with REX_DEBUG=1 for full details.", file=sys.stderr)


def wrap_entrypoint(fn: F) -> F:
    """Decorator: wrap *fn* with a global unhandled-exception handler.

    * Any :class:`Exception` → logged with full context, user-friendly
      message printed to stderr, then ``sys.exit(1)``.
    * :class:`SystemExit` and :class:`KeyboardInterrupt` pass through unchanged.
    """

    @wraps(fn)
    def _wrapper(*args: Any, **kwargs: Any) -> Any:
        try:
            return fn(*args, **kwargs)
        except (SystemExit, KeyboardInterrupt):
            raise
        except Exception as exc:  # noqa: BLE001
            handle_unhandled_exception(exc)
            _print_user_error(exc)
            sys.exit(1)

    return _wrapper  # type: ignore[return-value]