Source code for afnio.logging_config

import logging
from contextlib import contextmanager


[docs] class NotebookInfoHandler(logging.Handler): """ Custom logging handler for Jupyter notebooks that prints INFO-level log messages with special formatting and color. Intended to make user-facing messages more visible and user-friendly in notebook environments. """ COLORS = { "blue": "\033[94m", "yellow": "\033[93m", "green": "\033[92m", "reset": "\033[0m", }
[docs] def emit(self, record): """ Emit a log record, formatting and coloring specified arguments if provided. Only colors arguments specified in the 'colors' attribute of the record. Prints the message with a blue '[afnio]' prefix. """ msg = self.format(record) colors = getattr(record, "colors", {}) # Replace each value in the message with its color if specified if colors: # We assume the log message uses %r or %s for the colored fields # and that the order of args matches the keys in 'colors' # We'll replace each arg's repr() in the message with colored version for idx, color in colors.items(): try: value = record.args[idx] colored = f"{self.COLORS.get(color, '')}{repr(value)}{self.COLORS['reset']}" # noqa: E501 msg = msg.replace(repr(value), colored, 1) except (IndexError, AttributeError): continue print(f"{self.COLORS['blue']}[afnio]{self.COLORS['reset']} {msg}")
[docs] def in_notebook(): """ Detect if the current Python environment is a Jupyter notebook. Returns: bool: True if running in a Jupyter notebook, False otherwise. """ try: from IPython import get_ipython shell = get_ipython().__class__.__name__ return shell in ("ZMQInteractiveShell", "Shell") except Exception: return False
# TODO: Fix notebook behaviour that prints logs in the wrong cell when exception occours
[docs] def configure_logging(verbosity: str = "info"): """ Configure logging for the afnio library. Sets up logging format and levels for CLI, scripts, and Jupyter notebooks. In a notebook, adds a custom handler for INFO-level logs to display user-facing messages with color and formatting. Args: verbosity (str): Logging level as a string ("info", "debug", etc.). """ if not isinstance(verbosity, str): raise TypeError("verbosity must be a string like 'info', 'debug', etc.") level = getattr(logging, verbosity.upper(), logging.INFO) if level <= logging.DEBUG: fmt = "%(asctime)s - %(name)-35s - %(levelname)-9s - %(message)s" else: fmt = "%(levelname)-9s: %(message)s" # Set root logger to WARNING (default for all libraries) logging.basicConfig( level=logging.WARNING, format=fmt, force=True, ) # Set only afnio logs to the desired level logging.getLogger("afnio").setLevel(level) # Add notebook handler for INFO logs if in notebook if in_notebook() and level == logging.INFO: handler = NotebookInfoHandler() handler.setLevel(logging.INFO) handler.addFilter(lambda record: record.levelno == logging.INFO) logging.getLogger().addHandler(handler) class NoInfoFilter(logging.Filter): """ Logging filter that suppresses INFO-level log records. Used to prevent duplicate INFO messages in notebook environments. """ def filter(self, record): """ Filter method to suppress INFO-level log records. Args: record (logging.LogRecord): The log record to filter. Returns: bool: True if the record should be logged, False otherwise. """ return record.levelno != logging.INFO for h in logging.getLogger().handlers: if not isinstance(h, NotebookInfoHandler): h.addFilter(NoInfoFilter())
[docs] @contextmanager def set_logger_level(logger_name, level): """ Context manager to temporarily set the logging level for a logger. Args: logger_name (str): Name of the logger to set the level for. level (int): Logging level to set (e.g., logging.DEBUG, logging.INFO). Example: >>> with set_logger_level("afnio.tellurio.run", logging.WARNING): >>> # code that should log only WARNING and above """ logger = logging.getLogger(logger_name) old_level = logger.level logger.setLevel(level) try: yield finally: logger.setLevel(old_level)