Due to several limitations of the standard library's logging module, I wrote my own.
Below are the most important parts. You can find the whole library here.
Any feedback is welcome.
# fancylog - A library for human readable logging.
#
# Copyright (C) 2017 HOMEINFO - Digitale Informationssysteme GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""A library for beautiful, readble logging."""
from datetime import datetime
from enum import Enum
from sys import stdout, stderr
from threading import Thread
from time import sleep
from traceback import format_exc
from blessings import Terminal
__all__ = [
'logging',
'LogLevel',
'LogEntry',
'Logger',
'TTYAnimation',
'LoggingClass']
TERMINAL = Terminal()
def logging(name=None, level=None, parent=None, file=None):
"""Decorator to attach a logger to the respective class."""
def wrap(obj):
"""Attaches the logger to the respective class."""
logger_name = obj.__name__ if name is None else name
obj.logger = Logger(logger_name, level=level, parent=parent, file=file)
return obj
return wrap
class LogLevel(Enum):
"""Logging levels."""
DEBUG = (10, '🔧', TERMINAL.bold)
SUCCESS = (20, '✓', TERMINAL.green)
INFO = (30, 'ℹ', TERMINAL.blue)
WARNING = (70, '⚠', TERMINAL.yellow)
ERROR = (80, '✗', TERMINAL.red)
CRITICAL = (90, '☢', lambda string: TERMINAL.bold(TERMINAL.red(string)))
FAILURE = (100, '☠', lambda string: TERMINAL.bold(TERMINAL.magenta(
string)))
def __init__(self, ident, symbol, format_):
"""Sets the identifier, symbol and color."""
self.ident = ident
self.symbol = symbol
self.format = format_
def __int__(self):
"""Returns the identifier."""
return self.ident
def __str__(self):
"""Returns the colored symbol."""
return self.format(self.symbol)
def __eq__(self, other):
try:
return int(self) == int(other)
except TypeError:
return NotImplemented
def __gt__(self, other):
try:
return int(self) > int(other)
except TypeError:
return NotImplemented
def __ge__(self, other):
return self > other or self == other
def __lt__(self, other):
try:
return int(self) < int(other)
except TypeError:
return NotImplemented
def __le__(self, other):
return self < other or self == other
def __hash__(self):
return hash((self.__class__, self.ident))
@property
def erroneous(self):
"""A log level is considered erroneous if it's identifier > 50."""
return self.ident > 50
class LogEntry(Exception):
"""A log entry."""
def __init__(self, *messages, level=LogLevel.ERROR, sep=None, color=None):
"""Sets the log level and the respective message(s)."""
super().__init__()
self.messages = messages
self.level = level
self.sep = ' ' if sep is None else sep
self.color = color
self.timestamp = datetime.now()
def __hash__(self):
"""Returns a unique hash."""
return hash(self._hash_tuple)
@property
def _hash_tuple(self):
"""Returns the tuple from which to create a unique hash."""
return (self.__class__, self.level, self.messages, self.timestamp)
@property
def message(self):
"""Returns the message elements joint by the selected separator."""
return self.sep.join(str(message) for message in self.messages)
@property
def text(self):
"""Returns the formatted message text."""
if self.color is not None:
return self.color(self.message)
return self.message
class Logger:
"""A logger that can be nested."""
CHILD_SEP = '→'
def __init__(self, name, level=None, parent=None, file=None):
"""Sets the logger's name, log level, parent logger,
log entry template and file.
"""
self.name = name
self.level = level or LogLevel.INFO
self.parent = parent
self.file = file
self.template = '{1.level} {0}: {1.text}'
def __str__(self):
"""Returns the logger's nested path as a string."""
return self.CHILD_SEP.join(logger.name for logger in self.path)
def __hash__(self):
"""Returns a unique hash."""
return hash(self.__class__, self.name)
def __enter__(self):
"""Returns itself."""
return self
def __exit__(self, _, value, __):
"""Logs risen log entries."""
if isinstance(value, LogEntry):
self.log_entry(value)
return True
return None
@property
def root(self):
"""Determines whether the logger is at the root level."""
return self.parent is None
@property
def path(self):
"""Yields the logger's path."""
if not self.root:
yield from self.parent.path
yield self
@property
def layer(self):
"""Returns the layer of the logger."""
return 0 if self.root else self.parent.layer + 1
def log_entry(self, log_entry):
"""Logs a log entry."""
if log_entry.level >= self.level:
if self.file is None:
file = stderr if log_entry.level.erroneous else stdout
else:
file = self.file
print(self.template.format(self, log_entry), file=file, flush=True)
return log_entry
def inherit(self, name, level=None, file=None):
"""Returns a new child of this logger."""
level = self.level if level is None else level
file = self.file if file is None else file
return self.__class__(name, level=level, parent=self, file=file)
def log(self, level, *messages, sep=None, color=None):
"""Logs messages of a certain log level."""
log_entry = LogEntry(*messages, level=level, sep=sep, color=color)
return self.log_entry(log_entry)
def debug(self, *messages, sep=None, color=None):
"""Logs debug messages, defaulting to a stack trace."""
if not messages:
messages = ('Stacktrace:', format_exc())
if sep is None:
sep = '\n'
return self.log(LogLevel.DEBUG, *messages, sep=sep, color=color)
def success(self, *messages, sep=None, color=None):
"""Logs success messages."""
return self.log(LogLevel.SUCCESS, *messages, sep=sep, color=color)
def info(self, *messages, sep=None, color=None):
"""Logs info messages."""
return self.log(LogLevel.INFO, *messages, sep=sep, color=color)
def warning(self, *messages, sep=None, color=None):
"""Logs warning messages."""
return self.log(LogLevel.WARNING, *messages, sep=sep, color=color)
def error(self, *messages, sep=None, color=None):
"""Logs error messages."""
return self.log(LogLevel.ERROR, *messages, sep=sep, color=color)
def critical(self, *messages, sep=None, color=None):
"""Logs critical messages."""
return self.log(LogLevel.CRITICAL, *messages, sep=sep, color=color)
def failure(self, *messages, sep=None, color=None):
"""Logs failure messages."""
return self.log(LogLevel.FAILURE, *messages, sep=sep, color=color)
Use example:
#! /usr/bin/env python3
"""divide.
Usage:
divide <dividend> <divisor> [options]
Options:
--help, -h Show this page.
"""
from docopt import docopt
from fancylog import LogLevel, LogEntry, Logger
def divide(dividend, divisor):
try:
return dividend / divisor
except ZeroDivisionError:
raise LogEntry('Cannot divide by zero.', level=LogLevel.ERROR)
def main(options):
with Logger('Division logger', level=LogLevel.SUCCESS) as logger:
try:
dividend = float(options['<dividend>'])
except ValueError:
logger.error('Divident is not a float.')
return
try:
divisor = float(options['<divisor>'])
except ValueError:
logger.error('Divisor is not a float.')
return
logger.success(divide(dividend, divisor))
if __name__ == '__main__':
main(docopt(__doc__))
self.level = level or LogLevel.INFO
when every other check of the sort is of the formself.level = LogLevel.INFO if level is None else level
? \$\endgroup\$total_ordering
? \$\endgroup\$functools.partialmethod
in upstream. \$\endgroup\$