Last active
December 30, 2025 21:22
-
-
Save cheginit/61e6f5a57eabc2ac8be49ae6b7fc0b75 to your computer and use it in GitHub Desktop.
A Python logger with optional support for Rich
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Logging utilities.""" | |
| from __future__ import annotations | |
| import logging | |
| import sys | |
| from pathlib import Path | |
| from typing import Literal | |
| __all__ = ["configure_logger", "logger"] | |
| logger = logging.getLogger("package_name") | |
| logger.setLevel(logging.DEBUG) # Let handlers control filtering | |
| logger.propagate = False | |
| _file_handler: logging.FileHandler | None = None | |
| _console_handler: logging.Handler | None = None | |
| if not logger.handlers: | |
| try: | |
| from rich.console import Console # pyright: ignore[reportMissingImports] | |
| from rich.logging import RichHandler # pyright: ignore[reportMissingImports] | |
| _console_handler = RichHandler( | |
| console=Console(stderr=True, force_jupyter=False, width=120), | |
| show_time=True, | |
| show_level=True, | |
| show_path=False, | |
| rich_tracebacks=True, | |
| tracebacks_show_locals=True, | |
| log_time_format="[%Y/%m/%d %H:%M:%S]", | |
| omit_repeated_times=False, | |
| ) | |
| _console_handler.setFormatter(logging.Formatter("%(message)s")) | |
| except ImportError: | |
| _console_handler = logging.StreamHandler(sys.stdout) | |
| _console_handler.setFormatter( | |
| logging.Formatter( | |
| fmt="[%(asctime)s] %(levelname)s\t%(message)s", datefmt="%Y/%m/%d %H:%M:%S" | |
| ) | |
| ) | |
| _console_handler.setLevel(logging.WARNING) | |
| logger.addHandler(_console_handler) | |
| def _validate_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int) -> int: | |
| """Validate and convert logging level to integer. | |
| Parameters | |
| ---------- | |
| level : str or int | |
| Logging level as string or integer. | |
| Returns | |
| ------- | |
| int | |
| Validated logging level as integer constant. | |
| Raises | |
| ------ | |
| ValueError | |
| If the provided level is not valid. | |
| TypeError | |
| If the provided level is not a string or integer. | |
| """ | |
| if isinstance(level, str): | |
| level_upper = level.upper() | |
| if level_upper not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"): | |
| msg = ( | |
| f"Invalid log level: {level}. Must be one of: DEBUG, INFO, WARNING, ERROR, CRITICAL" | |
| ) | |
| raise ValueError(msg) | |
| return getattr(logging, level_upper) | |
| if isinstance(level, int): | |
| if level not in ( | |
| logging.DEBUG, | |
| logging.INFO, | |
| logging.WARNING, | |
| logging.ERROR, | |
| logging.CRITICAL, | |
| ): | |
| msg = f"Invalid log level: {level}. Must be a valid logging level constant." | |
| raise ValueError(msg) | |
| return level | |
| msg = f"Level must be str or int, got {type(level).__name__}" | |
| raise TypeError(msg) | |
| def configure_logger( | |
| *, | |
| verbose: bool | None = None, | |
| level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int | None = None, | |
| file: str | Path | None = None, | |
| file_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int | None = None, | |
| file_mode: Literal["a", "w"] = "a", | |
| ) -> None: | |
| """Configure logging settings. | |
| Parameters | |
| ---------- | |
| verbose : bool, optional | |
| Shortcut to set console level to DEBUG (True) or WARNING (False). | |
| If both ``level`` and ``verbose`` are provided, ``level`` takes precedence. | |
| level : str or int, optional | |
| Set console logging level. Valid options: ``DEBUG``, ``INFO``, ``WARNING``, | |
| ``ERROR``, ``CRITICAL``, or their integer equivalents. | |
| file : str or Path, optional | |
| Path to log file. If provided, enables file logging. | |
| Pass ``None`` to disable file logging. | |
| file_level : str or int, optional | |
| Logging level for file handler. Defaults to DEBUG if not specified. | |
| file_mode : {'a', 'w'}, optional | |
| File mode: 'a' for append, 'w' for overwrite. Defaults to 'a'. | |
| Raises | |
| ------ | |
| ValueError | |
| If invalid level or file_level is provided. | |
| TypeError | |
| If level or file_level is not a string or integer. | |
| Notes | |
| ----- | |
| The logger itself is set to DEBUG level, allowing handlers to independently | |
| control what messages they receive. This means file logging can capture | |
| DEBUG messages even when console is set to WARNING. | |
| Examples | |
| -------- | |
| >>> # Enable verbose logging | |
| >>> configure_logger(verbose=True) | |
| >>> # Set specific level | |
| >>> configure_logger(level="INFO") | |
| >>> # Enable file logging (captures all levels by default) | |
| >>> configure_logger(verbose=True, file="debug.log") | |
| >>> # Console shows warnings, file captures everything | |
| >>> configure_logger(verbose=False, file="full.log", file_level="DEBUG") | |
| >>> # File logging with custom level | |
| >>> configure_logger(file="errors.log", file_level="ERROR", file_mode="w") | |
| >>> # Disable file logging | |
| >>> configure_logger(file=None) | |
| """ | |
| global _file_handler # noqa: PLW0603 | |
| if level is not None: | |
| level_int = _validate_level(level) | |
| if _console_handler is not None: | |
| _console_handler.setLevel(level_int) | |
| elif verbose is not None: | |
| if _console_handler is not None: | |
| _console_handler.setLevel(logging.DEBUG if verbose else logging.WARNING) | |
| if file is not None: | |
| if _file_handler is not None: | |
| logger.removeHandler(_file_handler) | |
| _file_handler.close() | |
| _file_handler = None | |
| file_level_int = _validate_level(file_level) if file_level is not None else logging.DEBUG | |
| filepath = Path(file) | |
| filepath.parent.mkdir(parents=True, exist_ok=True) | |
| if file_mode not in ("a", "w"): | |
| raise ValueError(f"Invalid file_mode: {file_mode}. Must be 'a' or 'w'.") | |
| _file_handler = logging.FileHandler(filepath, mode=file_mode) | |
| _file_handler.setLevel(file_level_int) | |
| _file_handler.setFormatter( | |
| logging.Formatter( | |
| fmt="[%(asctime)s] %(levelname)-8s %(message)s", datefmt="%Y/%m/%d %H:%M:%S" | |
| ) | |
| ) | |
| logger.addHandler(_file_handler) | |
| elif file is None and _file_handler is not None: | |
| logger.removeHandler(_file_handler) | |
| _file_handler.close() | |
| _file_handler = None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment