Source code for modelly.log

"""This provides logging functionality for MODELLY.

It is modeled on the default `logging approach that comes with Python <https://docs.python.org/library/logging.html>`_.


"""

import logging
import sys
from functools import wraps
from logging import DEBUG, INFO
from typing import ClassVar

__all__ = [
    "DEBUG",
    "DEFAULT_LEVEL",
    "INFO",
    "LOGGER_NAME",
    "function_logger",
    "get_module_logger",
    "get_rootlogger",
    "log_to_stderr",
    "method_logger",
]
LOGGER_NAME = "MODELLY"
DEFAULT_LEVEL = DEBUG


def create_module_logger(name: str | None = None):
    """Helper function for creating a module logger.

    Args:
        name (str): The name to be given to the logger. If the name is None, the name defaults to the name of the module.

    """
    if name is None:
        # The caller's module globals; inspect.stack() reads source files and is slow.
        name = sys._getframe(1).f_globals["__name__"]
    logger = logging.getLogger(f"{LOGGER_NAME}.{name}")

    _module_loggers[name] = logger
    return logger


[docs] def get_module_logger(name: str): """Helper function for getting the module logger. Args: name (str): The name of the module in which the method being decorated is located """ try: logger = _module_loggers[name] except KeyError: logger = create_module_logger(name) return logger
_rootlogger = None _module_loggers = {} _logger = get_module_logger(__name__) class MODELLYColorFormatter(logging.Formatter): """Custom formatter for color based formatting.""" grey = "\x1b[38;20m" green = "\x1b[32m" yellow = "\x1b[33;20m" red = "\x1b[31;20m" bold_red = "\x1b[31;1m" reset = "\x1b[0m" format = ( "[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]" ) FORMATS: ClassVar[dict[int, str]] = { logging.DEBUG: grey + format + reset, logging.INFO: green + format + reset, logging.WARNING: yellow + format + reset, logging.ERROR: red + format + reset, logging.CRITICAL: bold_red + format + reset, } def format(self, record): """Format record.""" log_fmt = self.FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt) return formatter.format(record)
[docs] def method_logger(name: str): """Decorator for adding logging to a method. Args: name (str): The name of the module in which the method being decorated is located """ logger = get_module_logger(name) # Applied in a class body, so the calling frame is named after the class. classname = sys._getframe(1).f_code.co_name def real_decorator(func): @wraps(func) def wrapper(*args, **kwargs): # hack, because log is applied to methods, we can get # object instance as first arguments in args logger.debug( f"calling {classname}.{func.__name__} with {args[1::]} and {kwargs}" ) res = func(*args, **kwargs) return res return wrapper return real_decorator
[docs] def function_logger(name): """Decorator for adding logging to a Function. Args: name (str): The name of the module in which the function being decorated is located """ logger = get_module_logger(name) def real_decorator(func): @wraps(func) def wrapper(*args, **kwargs): logger.debug(f"calling {func.__name__} with {args} and {kwargs}") res = func(*args, **kwargs) return res return wrapper return real_decorator
[docs] def get_rootlogger(): """Returns root logger used by MODELLY. Returns: the root logger of MODELLY """ global _rootlogger # noqa: PLW0603 if not _rootlogger: _rootlogger = logging.getLogger(LOGGER_NAME) _rootlogger.handlers = [] _rootlogger.addHandler(logging.NullHandler()) _rootlogger.setLevel(DEBUG) return _rootlogger
[docs] def log_to_stderr( level: int | None = None, pass_root_logger_level: bool = False, propagate: bool = False, ): """Turn on logging and add a handler which prints to stderr. Args: level: minimum level of the messages that will be logged pass_root_logger_level: bool, optional. Default False if True, all module loggers will be set to the same logging level as the root logger. propagate: bool, optional. Default False if True, all module loggers will continue propagating records to ancestor loggers. """ if not level: level = DEFAULT_LEVEL logger = get_rootlogger() # avoid creation of multiple stream handlers for logging to console for entry in logger.handlers: if (isinstance(entry, logging.StreamHandler)) and ( isinstance(entry.formatter, MODELLYColorFormatter) ): return logger formatter = MODELLYColorFormatter() handler = logging.StreamHandler() handler.setLevel(level) handler.setFormatter(formatter) logger.addHandler(handler) logger.propagate = propagate if pass_root_logger_level: for _, mod_logger in _module_loggers.items(): mod_logger.setLevel(level) return logger