Source code for cliasi.logging_handler

"""
Logging handler for cliasi.

This module provides a custom logging handler that integrates with the Cliasi instance.
"""

import logging
import os
import sys
import traceback
from types import TracebackType

from . import STDERR_STREAM
from .cliasi import Cliasi


[docs] class CLILoggingHandler(logging.Handler): """ Logging handler that forwards records to the cliasi.cli Cliasi instance """ cli: Cliasi def __init__(self, cli_instance: Cliasi): """ Initialize the logging handler with a Cliasi instance :param cli_instance: Cliasi instance (default cli instance) """ super().__init__() self.cli = cli_instance self.setFormatter(logging.Formatter("%(message)s"))
[docs] def emit(self, record: logging.LogRecord) -> None: """ Emit a log record to the Cliasi instance :param record: logging.LogRecord :return: None """ try: msg = self.format(record) level = record.levelno if level >= logging.ERROR: exc_text = "" if record.exc_info: # Unpack exc_info tuple explicitly (static analysis and clarity) exc_type, exc_value, exc_tb = record.exc_info exc_text = "".join( traceback.format_exception(exc_type, exc_value, exc_tb) ) # pass verbosity so Cliasi can filter self.cli.fail(msg + exc_text, verbosity=level) elif level >= logging.WARNING: self.cli.warn(msg, verbosity=level) elif level >= logging.INFO: self.cli.info(msg, verbosity=level) else: self.cli.log_small(msg, verbosity=level) except Exception: STDERR_STREAM.write("! [cliasi] Failed to emit log record\n") STDERR_STREAM.write(traceback.format_exc() + "\n")
[docs] def install_logger(cli_instance: Cliasi, replace_root_handlers: bool = False) -> None: """ Install the CLILoggingHandler to the root logger :param cli_instance: Cliasi instance (default cli instance) :param replace_root_handlers: If True, existing StreamHandlers will be removed from the root logger so that only cliasi will print to the console. If False, existing StreamHandlers are left unchanged. :return: None """ handler = CLILoggingHandler(cli_instance) handler.setLevel(logging.NOTSET) root = logging.getLogger() for h in list(root.handlers): if isinstance(h, CLILoggingHandler): root.removeHandler(h) if replace_root_handlers: for h in list(root.handlers): if isinstance(h, logging.StreamHandler): root.removeHandler(h) if handler not in root.handlers: root.addHandler(handler) root.setLevel(logging.NOTSET)
[docs] def install_exception_hook(cli_instance: Cliasi) -> None: """ Install a global exception hook that logs uncaught exceptions to the Cliasi instance :param cli_instance: Cliasi instance (default cli instance) """ def handle_exception( exc_type: type[BaseException], exc_value: BaseException, exc_traceback: TracebackType | None, ) -> None: # Preserve KeyboardInterrupt behavior try: if isinstance(exc_type, type) and issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return except Exception: # If exc_type is malformed for issubclass, # continue to treat it as a non-KeyboardInterrupt pass # Detect whether the exception originated # from the cliasi package to avoid recursive failures tb = exc_traceback from_cliasi = False package_dir = os.path.dirname(__file__) while tb is not None: frame = tb.tb_frame module_name = frame.f_globals.get("__name__", "") filename = ( frame.f_code.co_filename if hasattr(frame.f_code, "co_filename") else "" ) if ( module_name.startswith("cliasi.") or module_name == "cliasi" or ( filename and os.path.commonpath([package_dir, filename]) == package_dir ) ): from_cliasi = True break tb = tb.tb_next if from_cliasi: # Avoid calling cli_instance.fail (which may itself raise). # Write a minimal message to stderr. try: STDERR_STREAM.write( "! [cliasi] Uncaught exception inside cliasi package;" " falling back to stderr\n" ) STDERR_STREAM.write( "".join( traceback.format_exception(exc_type, exc_value, exc_traceback) ) ) except Exception: # Last-resort fallback: # print nothing else to avoid raising inside the exception hook pass return # Normal path: use the Cliasi instance to report the exception, # but guard against any errors try: cli_instance.fail( "Uncaught exception:", verbosity=logging.ERROR, messages_stay_in_one_line=False, ) exception_text = "".join( traceback.format_exception(exc_type, exc_value, exc_traceback) ) cli_instance.fail( exception_text, verbosity=logging.ERROR, messages_stay_in_one_line=False, ) except Exception: # If Cliasi methods fail while handling the exception, # fallback to stderr so we don't raise again try: STDERR_STREAM.write( "! [cliasi] Failed while reporting uncaught exception" " via Cliasi; writing to stderr\n" ) STDERR_STREAM.write( "".join( traceback.format_exception(exc_type, exc_value, exc_traceback) ) ) except Exception: # Swallow any errors from fallback reporting to avoid infinite loops pass sys.excepthook = handle_exception