"""Custom ``zea`` python logging module.
Wrapper around python logging module to provide a simple interface for logging both
to the console and to a file with color support.
Example usage
^^^^^^^^^^^^^^
.. code-block:: python
from zea import log
log.info("This is an info message")
path = "data/datafile.hdf5"
log.info(f"Saved to {log.yellow(path)}")
"""
import contextlib
import logging
import os
import re
import sys
from pathlib import Path
# The logger to use
logger = None
file_logger = None
LOG_DIR = Path("log")
ZEA_LOG_LEVEL = os.getenv("ZEA_LOG_LEVEL", "DEBUG").upper()
DEPRECATED_LEVEL_NUM = logging.WARNING + 5
logging.addLevelName(DEPRECATED_LEVEL_NUM, "DEPRECATED")
logging.DEPRECATED = DEPRECATED_LEVEL_NUM
[docs]
def red(string):
"""Adds ANSI escape codes to print a string in red around the string."""
return "\033[31m" + str(string) + "\033[0m"
[docs]
def green(string):
"""Adds ANSI escape codes to print a string in green around the string."""
return "\033[32m" + str(string) + "\033[0m"
[docs]
def yellow(string):
"""Adds ANSI escape codes to print a string in yellow around the string."""
return "\033[33m" + str(string) + "\033[0m"
[docs]
def blue(string):
"""Adds ANSI escape codes to print a string in blue around the string."""
return "\033[34m" + str(string) + "\033[0m"
[docs]
def magenta(string):
"""Adds ANSI escape codes to print a string in magenta around the string."""
return "\033[35m" + str(string) + "\033[0m"
[docs]
def cyan(string):
"""Adds ANSI escape codes to print a string in cyan around the string."""
return "\033[36m" + str(string) + "\033[0m"
[docs]
def white(string):
"""Adds ANSI escape codes to print a string in white around the string."""
return "\033[37m" + str(string) + "\033[0m"
[docs]
def purple(string):
"""Adds ANSI escape codes to print a string in purple around the string."""
return "\033[38;5;93m" + str(string) + "\033[0m"
[docs]
def darkgreen(string):
"""Adds ANSI escape codes to print a string in blue around the string."""
return "\033[38;5;36m" + str(string) + "\033[0m"
[docs]
def orange(string):
"""Adds ANSI escape codes to print a string in orange around the string."""
return "\033[38;5;214m" + str(string) + "\033[0m"
[docs]
def bold(string):
"""Adds ANSI escape codes to print a string in bold around the string."""
return "\033[1m" + str(string) + "\033[0m"
[docs]
def remove_color_escape_codes(text):
"""
Removes ANSI color escape codes from the given string.
"""
# ANSI escape code pattern (e.g., \x1b[31m for red)
escape_code_pattern = re.compile(r"\x1b\[[0-9;]*m")
return escape_code_pattern.sub("", text)
[docs]
def success(message):
"""Prints a message to the console in green."""
logger.info(green(message))
if file_logger:
file_logger.info(remove_color_escape_codes(message))
return message
[docs]
def warning(message, *args, **kwargs):
"""Prints a message with log level warning."""
logger.warning(message, *args, **kwargs)
if file_logger:
file_logger.warning(remove_color_escape_codes(message), *args, **kwargs)
return message
[docs]
def deprecated(message, *args, **kwargs):
"""Prints a message with custom log level DEPRECATED."""
logger.log(DEPRECATED_LEVEL_NUM, message, *args, **kwargs)
if file_logger:
file_logger.log(DEPRECATED_LEVEL_NUM, remove_color_escape_codes(message), *args, **kwargs)
return message
[docs]
def error(message, *args, **kwargs):
"""Prints a message with log level error."""
logger.error(message, *args, **kwargs)
if file_logger:
file_logger.error(remove_color_escape_codes(message), *args, **kwargs)
return message
[docs]
def debug(message, *args, **kwargs):
"""Prints a message with log level debug."""
logger.debug(message, *args, **kwargs)
if file_logger:
file_logger.debug(remove_color_escape_codes(message), *args, **kwargs)
return message
[docs]
def info(message, *args, **kwargs):
"""Prints a message with log level info."""
logger.info(message, *args, **kwargs)
if file_logger:
file_logger.info(remove_color_escape_codes(message), *args, **kwargs)
return message
[docs]
def critical(message, *args, **kwargs):
"""Prints a message with log level critical."""
logger.critical(message, *args, **kwargs)
if file_logger:
file_logger.critical(message, *args, **kwargs)
return message
[docs]
def number_to_str(number, decimals=2):
"""Formats a number to a string with the given number of decimals."""
if isinstance(number, (int, float)):
return f"{number:.{decimals}f}"
else:
raise ValueError(f"Expected a number, got {type(number)}: {number}")
[docs]
def set_file_logger_directory(directory):
"""Sets the log level of the logger."""
global LOG_DIR, file_logger
LOG_DIR = directory
# Remove all handlers from the file logger
for handler in file_logger.handlers:
file_logger.removeHandler(handler)
# Add file handler
file_logger = configure_file_logger(level="DEBUG")
[docs]
def enable_file_logging():
"""Enables file logging"""
global file_logger
if not file_logger:
file_logger = configure_file_logger(level="DEBUG")
file_logger.propagate = False
[docs]
@contextlib.contextmanager
def set_level(level):
"""Context manager to temporarily set the log level for the logger.
Also sets the log level for the file logger if it exists.
Example:
>>> from zea import log
>>> with log.set_level("WARNING"):
... log.info("This will not be shown")
... log.warning("This will be shown")
Args:
level (str or int): The log level to set temporarily
(e.g., "DEBUG", "INFO", logging.WARNING).
Yields:
None
"""
prev_level = logger.level
prev_file_level = file_logger.level if file_logger else None
logger.setLevel(level)
if file_logger:
file_logger.setLevel(level)
try:
yield
finally:
logger.setLevel(prev_level)
if file_logger and prev_file_level is not None:
file_logger.setLevel(prev_file_level)
logger = configure_console_logger(
level=ZEA_LOG_LEVEL,
name="zea",
color=True,
name_color="darkgreen",
)
# File logger is disabled by default
file_logger = None
# Do not propagate the log messages to the root logger
# Prevents double logging when using the logger in multiple modules
logger.propagate = False