#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Logging utilities."""
import logging
import os
import sys
import click
import termcolor
# get the default root logger of Bob
_logger = logging.getLogger("bob")
# by default, warning and error messages should be written to sys.stderr
_warn_err = logging.StreamHandler(sys.stderr)
_warn_err.setLevel(logging.WARNING)
_logger.addHandler(_warn_err)
# debug and info messages are written to sys.stdout
class _InfoFilter:
def filter(self, record):
return record.levelno <= logging.INFO
_debug_info = logging.StreamHandler(sys.stdout)
_debug_info.setLevel(logging.DEBUG)
_debug_info.addFilter(_InfoFilter())
_logger.addHandler(_debug_info)
COLORMAP = dict(
debug=dict(),
info=dict(attrs=["bold"]),
warn=dict(color="yellow", attrs=["bold"]),
warning=dict(color="yellow", attrs=["bold"]),
error=dict(color="red"),
exception=dict(color="red", attrs=["bold"]),
critical=dict(color="red", attrs=["bold"]),
)
"""Default color map for homogenized color display"""
def _supports_color():
"""Returns True if the running system's terminal supports color, and False
otherwise."""
plat = sys.platform
supported_platform = plat != "Pocket PC" and (
plat != "win32" or "ANSICON" in os.environ
)
# isatty is not always implemented, #6223.
is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
if not supported_platform or not is_a_tty:
return False
return True
[docs]class ColorLog(object):
"""Colorizes logging colors."""
def __init__(self, logger):
self._log = logger
def __getattr__(self, name):
if name in [
"debug",
"info",
"warn",
"warning",
"error",
"exception",
"critical",
]:
if _supports_color():
return lambda s, *args: getattr(self._log, name)(
termcolor.colored(s, **COLORMAP[name]), *args
)
else:
return lambda s, *args: getattr(self._log, name)(s, *args)
return getattr(self._log, name)
[docs]def get_logger(name):
"""Returns the default logger as setup by this module."""
return ColorLog(logging.getLogger(name))
def _echo(text, *args, **kwargs):
"""Provides a colorized version of :py:func:`click.echo` (for terminals)
The color is stripped off if outputting to a file or piping the results of
a command using this function.
Parameters:
text (str): The text to be printed
args (tuple): Tuple of attributes directly passed to
:py:func:`termcolor.colored`
kwargs (dict): Dictionary of attributes directly passed to
:py:func:`termcolor.colored`
"""
click.echo(termcolor.colored(text, *args, **kwargs))
[docs]def echo_normal(text):
"""Color preset for normal text output for :py:func:`click.echo`"""
click.echo(text)
[docs]def echo_info(text):
"""Color preset for normal text output for :py:func:`click.echo`"""
_echo(text, "green")
[docs]def echo_warning(text):
"""Color preset for normal warning output for :py:func:`click.echo`"""
_echo(text, **COLORMAP["warn"])
# helper functions to instantiate and set-up logging
[docs]def setup(
logger_name, format="%(levelname)s:%(name)s@%(asctime)s: %(message)s"
):
"""This function returns a logger object that is set up to perform logging
using Bob loggers.
Parameters
----------
logger_name : str
The name of the module to generate logs for
format : :obj:`str`, optional
The format of the logs, see :py:class:`logging.LogRecord` for more
details. By default, the log contains the logger name, the log time, the
log level and the massage.
Returns
-------
logger : :py:class:`logging.Logger`
The logger configured for logging. The same logger can be retrieved using
the :py:func:`logging.getLogger` function.
"""
# generate new logger object
logger = logging.getLogger(logger_name)
# add log the handlers if not yet done
if not logger_name.startswith("bob") and not logger.handlers:
logger.addHandler(_warn_err)
logger.addHandler(_debug_info)
# this formats the logger to print the desired information
formatter = logging.Formatter(format)
# we have to set the formatter to all handlers registered in the current
# logger
for handler in logger.handlers:
handler.setFormatter(formatter)
# set the same formatter for bob loggers
for handler in _logger.handlers:
handler.setFormatter(formatter)
return ColorLog(logger)
[docs]def set_verbosity_level(logger, level):
"""Sets the log level for the given logger.
Parameters
----------
logger : :py:class:`logging.Logger` or str
The logger to generate logs for, or the name of the module to generate
logs for.
level : int
Possible log levels are: 0: Error; 1: Warning; 2: Info; 3: Debug.
Raises
------
ValueError
If the level is not in range(0, 4).
"""
if level not in range(0, 4):
raise ValueError(
"The verbosity level %d does not exist. Please reduce the number of "
"'--verbose' parameters in your command line" % level
)
# set up the verbosity level of the logging system
log_level = {
0: logging.ERROR,
1: logging.WARNING,
2: logging.INFO,
3: logging.DEBUG,
}[level]
# set this log level to the logger with the specified name
if isinstance(logger, str):
logger = logging.getLogger(logger)
logger.setLevel(log_level)
# set the same log level for the bob logger
_logger.setLevel(log_level)
[docs]def verbosity_option(**kwargs):
"""Adds a -v/--verbose option to a click command.
Parameters
----------
**kwargs
All kwargs are passed to click.option.
Returns
-------
callable
A decorator to be used for adding this option.
"""
global _logger
import click
def custom_verbosity_option(f):
def callback(ctx, param, value):
ctx.meta["verbosity"] = value
set_verbosity_level(_logger, value)
_logger.debug("`bob' logging level set to %d", value)
return value
return click.option(
"-v",
"--verbose",
count=True,
expose_value=False,
default=0,
help="Increase the verbosity level from 0 (only error messages) "
"to 1 (warnings), 2 (info messages), 3 (debug information) by "
"adding the --verbose option as often as desired "
"(e.g. '-vvv' for debug).",
callback=callback,
**kwargs,
)(f)
return custom_verbosity_option