"""This module defines logging capabilities for koris."""
import logging
import sys
import time
# pylint: disable=no-name-in-module
from koris.util.hue import (bad, red, info as infomsg, yellow, run, grey,
que, good, green)
LOG_LEVELS = list(range(5))
DEFAULT_LOG_LEVEL = 3
[docs]def get_logger(name):
"""Returns a Python logger.
Right now, only a single handler which logs to STDOUT can be added to a
logger. This is because if multiple calls with the same name would add
duplicate handlers to a logger, which lead to extra prints.
Args:
name (str): The name of the Logger.
Returns:
A Python Logger.
"""
log = logging.getLogger(name)
set_level(log, Logger.LOG_LEVEL)
# If we instantiate multiple loggers with the same name,
# we would add duplicate handlers.
if not log.handlers:
sh = logging.StreamHandler(sys.stdout)
fmt = logging.Formatter("%(message)s")
sh.setFormatter(fmt)
log.addHandler(sh)
return log
[docs]def set_level(logger, level):
"""Sets the logging level.
See `Python Logging Levels
<https://docs.python.org/3.6/library/logging.html#levels>`_ for
more information on how the koris levels relate to the original
Python levels.
Args:
logger: A Python logger object.
level (int): The logging level.
Raises:
ValueError if log level is unsupported.
"""
if level not in LOG_LEVELS:
raise ValueError(f"log level {level} is not supported")
logger.disabled = False
if level == 1:
logger.setLevel(40)
elif level == 2:
logger.setLevel(30)
elif level == 3:
logger.setLevel(20)
elif level == 4:
logger.setLevel(10)
else:
logger.disabled = True
[docs]class Singleton(type):
"""Metaclass to implement the Singleton pattern.
This Metaclass implements the Singleton pattern. This should only be used
logging purposes to avoid introducing mutable global state into the
application.
Metaclasses are classes that instantiate other classes. Everytime a new
class (any class in Python) is instantiated, it checks what the Metaclass
of that specific class is, then executes it with certain parameters.
In our case, the metaclass holds a dictionary that keeps track of all the
instances that have been created by the Singleton Metaclass. Everytime we
instantiate or call let's say :class:`koris.util.logger.Logger`, whose
Metaclass is the Singleton class, we check if we have already have such an
instance. If yes, that one is returned. If not, we create such an instance,
add it to the ``_instances`` dict and then return it. Subsequent calls will
then only return one instance, which is always the same object with the same ID.
For more information about the pattern, see `Eli's Post
<https://eli.thegreenplace.net/2011/08/14/python-metaclasses-by-example/>`_
and `Stack Overflow <https://stackoverflow.com/a/6798042>`_.
Example:
>>> log1 = Logger(__name__)
>>> log2 = Logger(__name__)
>>> log1.info("hello")
[~] hello
>>> log2.info("world")
[~] world
>>> log3 = Logger("koris")
>>> id(log1) == id(log2) == id(log3)
True
"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
else:
cls._instances[cls].__init__(*args, **kwargs)
return cls._instances[cls]
[docs]class Logger(metaclass=Singleton):
"""This class provides logging capabilities.
This class is a singleton that returns as proxy instance of
logging.Logger.
Before using, make sure to set Logger.LOG_LEVEL to the desired
level.
The different levels are:
.. code:: shell
* 0 - quiet (no output)
* 1 - error
* 2 - warning
* 3 - info
* 4 - debug
A logger initiated with a specific level will print everything below
(except 0) but not above.
All functions except for :meth`.Logger.question` support ``f``-, ``%``-,
and ``format``-Style formatting.
Example:
>>> log = Logger(__name__)
>>> log.info("hello world")
[~] hello world
>>> log.info("%s %s", "hello", "world")
[~] hello world
>>> a, b = "hello", "world"
>>> log.info(f"{a} {b}")
[~] hello world
>>> log.info("{} {}".format(b, a))
[~] world hello
Attributes:
LOG_LEVEL (int): The log level to be used across the application.
Args:
name (str): The name of the logger.
"""
LOG_LEVEL = DEFAULT_LOG_LEVEL
def __init__(self, name):
self.logger = get_logger(name)
@property
def level(self):
"""Returns the Python log level equivalent.
Returns:
The Python loglevel equivalent or None if logger not instantiated.
"""
if not self.logger:
return None
if self.logger.disabled:
return 0
return self.logger.level
@level.setter
def level(self, level):
level_to_int = {
'quiet': 0,
'error': 1,
'warning': 2,
'info': 3,
'debug': 4}
try:
level = level_to_int[level]
except KeyError:
level = int(level)
set_level(self.logger, level)
[docs] def error(self, msg, *args, color=True, **kwargs):
"""Logs a message on error level.
If color is True, will be logged in red with ``[-]``, else
in plain.
Example:
>>> log.error("test")
[-] test
>>> log.error("test", color=False)
test
Args:
msg (str): The message to be logged.
color (bool): If the message should be colored.
"""
if color:
msg = bad(red(msg))
self.logger.error(msg, *args, **kwargs)
[docs] def warning(self, msg, *args, color=True, **kwargs):
"""Logs a message on warning level.
If color is True, will be logged in yellow with ``[!]``, else
in plain.
Example:
>>> log.warning("test")
[!] test
>>> log.warning("test", color=False)
test
Args:
msg (str): The message to be logged.
color (bool): If the message should be colored.
"""
if color:
msg = infomsg(yellow(msg))
self.logger.warning(msg, *args, **kwargs)
[docs] def warn(self, msg, *args, color=True, **kwargs):
"""Convenience function to log on warning level.
Will just call :meth:`.Logger.warning`.
Args:
msg (str): The message to be logged.
color (bool): If the message should be colored.
"""
self.warning(msg, *args, **kwargs, color=color)
[docs] def info(self, msg, *args, color=True, **kwargs):
"""Logs a message on info level.
If color is True, will be logged in grey with ``[~]``, else
in plain.
Example:
>>> log.info("test")
[~] test
>>> log.info("test", color=False)
test
Args:
msg (str): The message to be logged.
color (bool): If the message should be colored.
"""
if color:
msg = run(grey(msg))
self.logger.info(msg, *args, **kwargs)
[docs] def debug(self, msg, *args, color=True, **kwargs):
"""Logs a message on debug level.
If color is True, will be logged in grey with the current
timestamp in brackets as prefix, else in plain.
Example:
>>> log.debug("test")
[20190426-155611] test
>>> log.debug("test", color=False)
test
Args:
msg (str): The message to be logged.
color (bool): If the message should be colored.
"""
if color:
now = time.strftime("%Y%m%d-%H%M%S")
msg = grey(f"[{now}] {msg}")
self.logger.debug(msg, *args, **kwargs)
[docs] def success(self, msg, *args, color=True, **kwargs):
"""Indicates a success.
Messages are printend on info level.
If color is True, will be logged in green with ``[+]``, else
in plain.
Example:
>>> log.info("test")
[~] test
>>> log.success("test")
[+] test
>>> log.success("test", color=False)
test
Args:
msg (str): The message to be logged.
color (bool): If the message should be colored
"""
if color:
msg = good(green(msg))
self.logger.info(msg, *args, **kwargs)
[docs] @staticmethod
def question(msg, color=True):
"""Outputs a question.
Questions are unaffected by the log level and always printed.
This function does not support the %-formatting syntax.
If color is True, will be logged in green with ``[+]``, else
in plain.
Example:
>>> log.question("test")
[?] test
>>> log.question("test", color=False)
test
Args:
msg (str): The message to be logged.
color (bool): If the message should be colored
"""
if color:
msg = que(msg)
print(msg)