# License: BSD 3 clause
"""
Functions related to logging in SKLL.
:author: Nitin Madnani (nmadnani@ets.org)
:organization: ETS
"""
import logging
import re
import warnings
from functools import partial
from os.path import sep
from typing import Optional
orig_showwarning = warnings.showwarning
SKLEARN_WARNINGS_RE = re.compile(re.escape(f"{sep}sklearn{sep}"))
class MatplotlibCategoryFilter(logging.Filter):
"""
Class to filter out specific log records from `matplotlib.category`.
This is useful when generating learning curves which generates unnecessary
log records from `matplotlib.category`. For more details, see this issue:
https://github.com/matplotlib/matplotlib/issues/23422
"""
def filter(self, record):
"""
Implement the filter method.
Parameters
----------
record : logging.LogRecord
The log record to be filtered.
"""
# Check if the log record is from matplotlib.category and contains the specific message
if (
record.name == "matplotlib.category"
and "Using categorical units to plot a list of strings" in record.msg
):
# filter out this record
return False
# allow other records through
return True
def send_sklearn_warnings_to_logger(
logger, message, category, filename, lineno, file=None, line=None
):
"""
Return method that sends `sklearn`-specific warnings to a logger.
This method that can be used to replace warnings.showwarning (via `partial`,
specifying a `logger` instance).
"""
if SKLEARN_WARNINGS_RE.search(filename):
logger.warning(f"{filename}:{lineno}: {category.__name__}:{message}")
else:
orig_showwarning(message, category, filename, lineno, file=file, line=line)
[docs]
def get_skll_logger(
name: str, filepath: Optional[str] = None, log_level: int = logging.INFO
) -> logging.Logger:
"""
Create and return logger instances appropriate for use in SKLL code.
These logger instances can log to both STDERR as well as a file. This
function will try to reuse any previously created logger based on the
given name and filepath.
Parameters
----------
name : str
The name to be used for the logger.
filepath : Optional[str], default=None
The file to be used for the logger via a FileHandler.
Default: None in which case no file is attached to the
logger.
log_level : int, default=logging.INFO
The level for logging messages
Returns
-------
logger: logging.Logger
A ``Logger`` instance.
"""
# first get the logger instance associated with the
# given name if one already exists
logger = logging.getLogger(name)
logger.setLevel(log_level)
# if we are given a file path and this existing logger doesn't already
# have a file handler for this file, then add one.
if filepath:
def is_file_handler(handler):
return isinstance(handler, logging.FileHandler) and handler.stream.name == filepath
need_file_handler = not any([is_file_handler(handler) for handler in logger.handlers])
if need_file_handler:
formatter = logging.Formatter("%(asctime)s - %(levelname)s - " "%(message)s")
file_handler = logging.FileHandler(filepath, mode="w")
file_handler.setFormatter(formatter)
file_handler.setLevel(log_level)
logger.addHandler(file_handler)
warnings.showwarning = partial(send_sklearn_warnings_to_logger, logger)
# return the logger instance
return logger
def close_and_remove_logger_handlers(logger: logging.Logger) -> None:
"""
Close and remove any handlers attached to a logger instance.
Parameters
----------
logger : logging.Logger
Logger instance
"""
for handler in logger.handlers[:]:
handler.close()
logger.removeHandler(handler)