#!/usr/bin/env python3
# ----------------------------------------------------------------------
# Copyright (C) 2024 Tristan Hoellinger
# Distributed under the GNU General Public License v3.0 (GPLv3).
# See the LICENSE file in the root directory for details.
# SPDX-License-Identifier: GPL-3.0-or-later
# ----------------------------------------------------------------------
__author__ = "Tristan Hoellinger"
__version__ = "0.1.0"
__date__ = "2024"
__license__ = "GPLv3"
"""
Logger routines for the SelfiSys package.
The printing routines and colours are adapted from the Simbelmynë
comological solver (https://simbelmyne.readthedocs.io/en/latest).
"""
import sys
from typing import cast
import logging
from selfisys import DEFAULT_VERBOSE_LEVEL
# Global variables for fonts
FONT_BOLDRED = "\033[1;31m"
FONT_BOLDGREEN = "\033[1;32m"
FONT_BOLDYELLOW = "\033[1;33m"
FONT_BOLDCYAN = "\033[1;36m"
FONT_BOLDGREY = "\033[1;37m"
FONT_LIGHTPURPLE = "\033[38;5;147m"
FONT_NORMAL = "\033[00m"
# Global variables for verbosity
ERROR_VERBOSITY = 0
INFO_VERBOSITY = 1
WARNING_VERBOSITY = 2
DIAGNOSTIC_VERBOSITY = 3
DEBUG_VERBOSITY = 4
DIAGNOSTIC_LEVEL = 15
logging.addLevelName(DIAGNOSTIC_LEVEL, "DIAGNOSTIC")
G__ind__ = 0 # Global variable for logger indentation
[docs]
def INDENT():
"""Indents the current level of outputs."""
global G__ind__
G__ind__ += 1
return G__ind__
[docs]
def UNINDENT():
"""Unindents the current level of outputs."""
global G__ind__
G__ind__ -= 1
return G__ind__
[docs]
def PrintLeftType(message_type, FONT_COLOR):
"""Prints the type of output to screen.
Parameters
----------
message_type (string) : type of message
FONT_COLOR (string) : font color for this type of message
"""
from time import localtime, strftime
sys.stdout.write(
"["
+ strftime("%H:%M:%S", localtime())
+ "|"
+ FONT_COLOR
+ message_type
+ FONT_NORMAL
+ "]"
)
sys.stdout.write("==" * G__ind__)
sys.stdout.write("|")
[docs]
def PrintInfo(message):
"""Prints an information to screen.
Parameters
----------
message (string) : message
"""
if DEFAULT_VERBOSE_LEVEL >= INFO_VERBOSITY:
PrintLeftType("INFO ", FONT_BOLDCYAN)
sys.stdout.write("{}\n".format(message))
sys.stdout.flush()
[docs]
def PrintDiagnostic(verbosity, message):
"""Prints a diagnostic to screen.
Parameters
----------
verbosity (int) : verbosity of the message
message (string) : message
"""
if DEFAULT_VERBOSE_LEVEL >= verbosity:
PrintLeftType("DIAGNOSTIC", FONT_BOLDGREY)
sys.stdout.write("{}\n".format(message))
[docs]
def PrintWarning(message):
"""Prints a warning to screen.
Parameters
----------
message (string) : message
"""
if DEFAULT_VERBOSE_LEVEL >= WARNING_VERBOSITY:
PrintLeftType("WARNING ", FONT_BOLDYELLOW)
sys.stdout.write(FONT_BOLDYELLOW + message + FONT_NORMAL + "\n")
[docs]
def PrintError(message):
"""Prints an error to screen.
Parameters
----------
message (string) : message
"""
if DEFAULT_VERBOSE_LEVEL >= ERROR_VERBOSITY:
PrintLeftType("ERROR ", FONT_BOLDRED)
sys.stdout.write(FONT_BOLDRED + message + FONT_NORMAL + "\n")
[docs]
class CustomLoggerHandler(logging.Handler):
"""
Custom logging handler to redirect Python logger messages to custom
print functions, with support for verbosity levels in debug
messages.
"""
[docs]
def emit(self, record):
"""
Emit a log record.
"""
try:
log_message = self.format(record)
log_level = record.levelno
if log_level >= logging.ERROR:
PrintError(log_message)
elif log_level >= logging.WARNING:
PrintWarning(log_message)
elif log_level >= logging.INFO:
PrintInfo(log_message)
elif log_level == DIAGNOSTIC_LEVEL:
# Retrieve verbosity level from the record
verbosity = getattr(record, "verbosity", DIAGNOSTIC_VERBOSITY)
PrintDiagnostic(verbosity=verbosity, message=log_message)
elif log_level >= logging.DEBUG:
PrintDiagnostic(verbosity=DEBUG_VERBOSITY, message=log_message)
else:
# Fallback for other levels
PrintInfo(log_message)
except Exception:
self.handleError(record)
[docs]
class CustomLogger(logging.Logger):
"""
Custom logger class supporting custom verbosity levels in diagnostic
messages.
"""
[docs]
def diagnostic(self, msg, *args, verbosity=DIAGNOSTIC_VERBOSITY, **kwargs) -> None:
"""
Log a message with DIAGNOSTIC level.
Parameters
----------
msg : str
The message to log.
verbosity : int, optional
The verbosity level required to log this message.
"""
if self.isEnabledFor(DIAGNOSTIC_LEVEL):
# Pass verbosity as part of the extra argument
extra = kwargs.get("extra", {})
extra["verbosity"] = verbosity
kwargs["extra"] = extra
self.log(DIAGNOSTIC_LEVEL, msg, *args, **kwargs)
logging.setLoggerClass(CustomLogger)
[docs]
def getCustomLogger(name: str) -> CustomLogger:
"""
Get as CustomLogger instance to use the custom printing routines.
Parameters
----------
name : str
The name of the logger.
Returns
-------
logger : logging.Logger
The custom logger instance.
"""
logging.setLoggerClass(CustomLogger)
logger = cast(CustomLogger, logging.getLogger(name)) # cast for type checkers and PyLance
logger.setLevel(logging.DEBUG) # Set the desired base logging level
handler = CustomLoggerHandler()
formatter = logging.Formatter(f"{FONT_LIGHTPURPLE}(%(name)s){FONT_NORMAL} %(message)s")
handler.setFormatter(formatter)
# Attach the handler to the logger if not already present
if not logger.handlers:
logger.addHandler(handler)
return logger