"""Logging utils."""
import logging
import sys
from enum import Enum
from pathlib import Path
from types import TracebackType
from typing import Self
[docs]
class LogLevelError(KeyError):
"""Error type for unsupported log levels."""
def __init__(self, s: str) -> None:
super().__init__(f"LogLevel '{s}' doesn't exist.")
[docs]
class LogLevel(Enum):
"""Log Level Enumeration."""
NOTSET = logging.NOTSET
DEBUG = logging.DEBUG
INFO = logging.INFO
WARNING = logging.WARNING
ERROR = logging.ERROR
CRITICAL = logging.CRITICAL
def __str__(self) -> str: # noqa: D105 docstring of __str__ isn't required: it must match the standard library's
return self.name
[docs]
@classmethod
def from_string(cls, s: str) -> Self:
"""Instantiate a LogLevel from a string."""
try:
return cls[s]
except KeyError as exc:
raise LogLevelError(s) from exc
[docs]
def log_uncaught_exceptions() -> None:
"""Make all uncaught exception to be logged by the default logger.
Keyboard exceptions and children classes are not logged so one can kill the program with ctr+C.
"""
def handle_exception(
exc_type: type[BaseException], exc_value: BaseException, exc_traceback: TracebackType | None
) -> None:
if not issubclass(exc_type, KeyboardInterrupt):
logger = logging.getLogger(__name__)
logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
sys.__excepthook__(exc_type, exc_value, exc_traceback)
sys.excepthook = handle_exception
[docs]
def init_logging(log_file: Path | None, log_level: LogLevel) -> None:
"""(Re-)initialize all loggers.
Parameters
----------
log_file : Path | None
Filename to write the log to. Default is None, in which case the log goes to standard output/error.
log_level : str | None
Logging level. Default is None, in which case the default of `logging` is used (WARNING).
"""
logging.captureWarnings(True) # log all warnings from the warnings module. # noqa: FBT003 API is not ours
log_uncaught_exceptions() # log all uncaught exceptions as well
logging_format = "%(asctime)s [%(levelname)s] %(name)s:%(lineno)s:%(funcName)s %(message)s"
handlers = [logging.FileHandler(log_file)] if log_file is not None else [logging.StreamHandler()]
logging.basicConfig(
level=log_level.value,
format=logging_format,
handlers=handlers,
force=True,
)
logger = logging.getLogger(__name__)
logger.info("Logging configured - start logging")