Source code for ascii_colors.logging

# -*- coding: utf-8 -*-
"""
Logging compatibility layer for ascii_colors.
Provides standard logging module compatibility.
"""

import sys
import io
import warnings
from typing import Any, Dict, List, Optional, Union, Callable, Tuple, Iterator

from ascii_colors.constants import LogLevel, DEBUG, INFO, WARNING, ERROR, CRITICAL, NOTSET
from ascii_colors.core import ASCIIColors
from ascii_colors.handlers import Handler, ConsoleHandler, FileHandler, RotatingFileHandler, StreamHandler
from ascii_colors.formatters import Formatter, JSONFormatter

# Expose context methods for compatibility
set_context = ASCIIColors.set_context
clear_context = ASCIIColors.clear_context
context = ASCIIColors.context
get_thread_context = ASCIIColors.get_thread_context


# Cache for logger adapters
_logger_cache: Dict[str, "_AsciiLoggerAdapter"] = {}


[docs] class _AsciiLoggerAdapter: """Adapter that provides standard logging.Logger interface using ASCIIColors backend.""" def __init__(self, name: str): self.name = name self.level = NOTSET self.handlers: List[Handler] = [] self.disabled = False self.propagate = True def _get_effective_level(self) -> int: """Get the effective level for this logger.""" if self.level != NOTSET: return self.level # Return global level from ASCIIColors return ASCIIColors._global_level.value
[docs] def isEnabledFor(self, level: int) -> bool: """ Check if this logger is enabled for the specified level. This is a standard logging.Logger method that checks whether a message at the given level would actually be logged. Args: level: The log level to check (e.g., DEBUG, INFO, WARNING) Returns: True if the logger is enabled for the given level, False otherwise """ # Check if logger is disabled if self.disabled: return False # Get effective level and compare effective_level = self._get_effective_level() return level >= effective_level
[docs] def setLevel(self, level: Union[int, LogLevel]) -> None: """Set the logging level.""" if isinstance(level, LogLevel): self.level = level.value else: self.level = level
[docs] def getEffectiveLevel(self) -> int: """Get the effective level for this logger.""" return self._get_effective_level()
[docs] def debug(self, msg: str, *args, **kwargs) -> None: """Log a debug message.""" if self.isEnabledFor(DEBUG): self._log(LogLevel.DEBUG, msg, args, **kwargs)
[docs] def info(self, msg: str, *args, **kwargs) -> None: """Log an info message.""" if self.isEnabledFor(INFO): self._log(LogLevel.INFO, msg, args, **kwargs)
[docs] def warning(self, msg: str, *args, **kwargs) -> None: """Log a warning message.""" if self.isEnabledFor(WARNING): self._log(LogLevel.WARNING, msg, args, **kwargs)
[docs] def warn(self, msg: str, *args, **kwargs) -> None: """Deprecated alias for warning.""" self.warning(msg, *args, **kwargs)
[docs] def error(self, msg: str, *args, **kwargs) -> None: """Log an error message.""" if self.isEnabledFor(ERROR): self._log(LogLevel.ERROR, msg, args, **kwargs)
[docs] def critical(self, msg: str, *args, **kwargs) -> None: """Log a critical message.""" if self.isEnabledFor(CRITICAL): self._log(LogLevel.CRITICAL, msg, args, **kwargs)
[docs] def fatal(self, msg: str, *args, **kwargs) -> None: """Log a fatal message (alias for critical).""" self.critical(msg, *args, **kwargs)
[docs] def exception(self, msg: str, *args, **kwargs) -> None: """Log an exception message with traceback.""" kwargs['exc_info'] = True self.error(msg, *args, **kwargs)
[docs] def log(self, level: int, msg: str, *args, **kwargs) -> None: """Log a message at the specified level.""" if self.isEnabledFor(level): log_level = LogLevel(level) if isinstance(level, int) else level self._log(log_level, msg, args, **kwargs)
def _log(self, level: LogLevel, msg: str, args: Tuple, **kwargs) -> None: """Internal logging method.""" # Get exception info if requested exc_info = kwargs.get('exc_info', False) # Format message here before passing to ASCIIColors._log # ASCIIColors._log expects pre-formatted message formatted_msg = (msg % args) if args else msg # Call ASCIIColors._log with logger name ASCIIColors._log( level=level, message=formatted_msg, args=(), # Already formatted, no args needed exc_info=exc_info, logger_name=self.name, **{k: v for k, v in kwargs.items() if k not in ('exc_info', 'stack_info', 'extra')} )
[docs] def addHandler(self, hdlr: Handler) -> None: """Add a handler to this logger.""" ASCIIColors.add_handler(hdlr) self.handlers.append(hdlr)
[docs] def removeHandler(self, hdlr: Handler) -> None: """Remove a handler from this logger.""" ASCIIColors.remove_handler(hdlr) if hdlr in self.handlers: self.handlers.remove(hdlr)
[docs] def hasHandlers(self) -> bool: """Check if this logger has any handlers.""" return len(self.handlers) > 0 or len(ASCIIColors._handlers) > 0
[docs] def filter(self, record: Any) -> bool: """Filter method (stub for compatibility).""" return True
[docs] def handle(self, record: Any) -> None: """Handle a log record.""" pass
[docs] def findCaller(self, stack_info: bool = False, stacklevel: int = 1) -> Tuple: """Find the caller (stub for compatibility).""" import inspect frame = inspect.currentframe() if frame: frame = frame.f_back if frame and frame.f_back: frame = frame.f_back co = frame.f_code return (co.co_filename, frame.f_lineno, co.co_name, None) return ("(unknown file)", 0, "(unknown function)", None)
[docs] def getChild(self, suffix: str) -> "_AsciiLoggerAdapter": """Get a child logger.""" if self.root: return getLogger(f"{self.name}.{suffix}") return getLogger(suffix)
@property def root(self) -> bool: """Check if this is the root logger.""" return self.name == "root" or not self.name def __repr__(self) -> str: return f"<_AsciiLoggerAdapter({self.name!r})>"
[docs] def getLogger(name: Optional[str] = None) -> _AsciiLoggerAdapter: """ Get a logger with the specified name. This function mirrors logging.getLogger() for drop-in compatibility. """ name = name or "root" if name not in _logger_cache: _logger_cache[name] = _AsciiLoggerAdapter(name) return _logger_cache[name]
[docs] def basicConfig( *, filename: Optional[str] = None, filemode: str = 'a', format: Optional[str] = None, datefmt: Optional[str] = None, style: str = '%', level: Optional[int] = None, stream: Optional[io.TextIOWrapper] = None, handlers: Optional[List[Handler]] = None, force: bool = False, ) -> None: """ Configure basic logging. Mirrors logging.basicConfig() for drop-in compatibility. """ # Check if already configured (thread-safe) with ASCIIColors._handler_lock: handler_count = len(ASCIIColors._handlers) already_configured = ASCIIColors._basicConfig_called # If already configured (either via basicConfig or manually adding handlers), do nothing unless forced if (already_configured or handler_count > 0) and not force: return # Mark as configured (thread-safe) with ASCIIColors._handler_lock: ASCIIColors._basicConfig_called = True if force: ASCIIColors.clear_handlers() _logger_cache.clear() # Set level if provided (only after we've determined we should configure) if level is not None: ASCIIColors.set_log_level(level) # Create formatter fmt = format or "%(levelname)s:%(name)s:%(message)s" formatter = Formatter(fmt=fmt, datefmt=datefmt, style=style) # Handle explicit handlers list if handlers: for hdlr in handlers: if hdlr.formatter is None: hdlr.setFormatter(formatter) ASCIIColors.add_handler(hdlr) else: # Create default console handler if filename: hdlr = FileHandler(filename, mode=filemode) else: hdlr = ConsoleHandler(stream=stream or sys.stderr) hdlr.setFormatter(formatter) ASCIIColors.add_handler(hdlr) ASCIIColors._basicConfig_called = True
[docs] def shutdown() -> None: """Shutdown the logging system.""" for handler in ASCIIColors._handlers[:]: try: handler.close() except Exception: pass ASCIIColors.clear_handlers() _logger_cache.clear()
# Convenience function for exception logging def trace_exception(ex: Exception, enhanced: bool = True, max_width: Optional[int] = None) -> None: """Log an exception with traceback.""" from ascii_colors.utils import get_trace_exception formatted = get_trace_exception(ex, enhanced=enhanced, max_width=max_width) ASCIIColors._log(LogLevel.ERROR, formatted, (), exc_info=None, logger_name='trace_exception')