# -*- coding: utf-8 -*- """ logbook.compat ~~~~~~~~~~~~~~ Backwards compatibility with stdlib's logging package and the warnings module. :copyright: (c) 2010 by Armin Ronacher, Georg Brandl. :license: BSD, see LICENSE for more details. """ import collections import logging import sys import warnings from datetime import date, datetime import logbook from logbook.helpers import u, string_types, iteritems, collections_abc _epoch_ord = date(1970, 1, 1).toordinal() def redirect_logging(set_root_logger_level=True): """Permanently redirects logging to the stdlib. This also removes all otherwise registered handlers on root logger of the logging system but leaves the other loggers untouched. :param set_root_logger_level: controls of the default level of the legacy root logger is changed so that all legacy log messages get redirected to Logbook """ del logging.root.handlers[:] logging.root.addHandler(RedirectLoggingHandler()) if set_root_logger_level: logging.root.setLevel(logging.DEBUG) class redirected_logging(object): """Temporarily redirects logging for all threads and reverts it later to the old handlers. Mainly used by the internal unittests:: from logbook.compat import redirected_logging with redirected_logging(): ... """ def __init__(self, set_root_logger_level=True): self.old_handlers = logging.root.handlers[:] self.old_level = logging.root.level self.set_root_logger_level = set_root_logger_level def start(self): redirect_logging(self.set_root_logger_level) def end(self, etype=None, evalue=None, tb=None): logging.root.handlers[:] = self.old_handlers logging.root.setLevel(self.old_level) __enter__ = start __exit__ = end class LoggingCompatRecord(logbook.LogRecord): def _format_message(self, msg, *args, **kwargs): if kwargs: assert not args return msg % kwargs else: assert not kwargs return msg % tuple(args) class RedirectLoggingHandler(logging.Handler): """A handler for the stdlib's logging system that redirects transparently to logbook. This is used by the :func:`redirect_logging` and :func:`redirected_logging` functions. If you want to customize the redirecting you can subclass it. """ def __init__(self): logging.Handler.__init__(self) def convert_level(self, level): """Converts a logging level into a logbook level.""" if level >= logging.CRITICAL: return logbook.CRITICAL if level >= logging.ERROR: return logbook.ERROR if level >= logging.WARNING: return logbook.WARNING if level >= logging.INFO: return logbook.INFO return logbook.DEBUG def find_extra(self, old_record): """Tries to find custom data from the old logging record. The return value is a dictionary that is merged with the log record extra dictionaries. """ rv = vars(old_record).copy() for key in ('name', 'msg', 'args', 'levelname', 'levelno', 'pathname', 'filename', 'module', 'exc_info', 'exc_text', 'lineno', 'funcName', 'created', 'msecs', 'relativeCreated', 'thread', 'threadName', 'greenlet', 'processName', 'process'): rv.pop(key, None) return rv def find_caller(self, old_record): """Tries to find the caller that issued the call.""" frm = sys._getframe(2) while frm is not None: if (frm.f_globals is globals() or frm.f_globals is logbook.base.__dict__ or frm.f_globals is logging.__dict__): frm = frm.f_back else: return frm def convert_time(self, timestamp): """Converts the UNIX timestamp of the old record into a datetime object as used by logbook. """ return datetime.utcfromtimestamp(timestamp) def convert_record(self, old_record): """Converts an old logging record into a logbook log record.""" args = old_record.args kwargs = None # Logging allows passing a mapping object, in which case args will be a mapping. if isinstance(args, collections_abc.Mapping): kwargs = args args = None record = LoggingCompatRecord(old_record.name, self.convert_level(old_record.levelno), old_record.msg, args, kwargs, old_record.exc_info, self.find_extra(old_record), self.find_caller(old_record)) record.time = self.convert_time(old_record.created) return record def emit(self, record): logbook.dispatch_record(self.convert_record(record)) class LoggingHandler(logbook.Handler): """Does the opposite of the :class:`RedirectLoggingHandler`, it sends messages from logbook to logging. Because of that, it's a very bad idea to configure both. This handler is for logbook and will pass stuff over to a logger from the standard library. Example usage:: from logbook.compat import LoggingHandler, warn with LoggingHandler(): warn('This goes to logging') """ def __init__(self, logger=None, level=logbook.NOTSET, filter=None, bubble=False): logbook.Handler.__init__(self, level, filter, bubble) if logger is None: logger = logging.getLogger() elif isinstance(logger, string_types): logger = logging.getLogger(logger) self.logger = logger def get_logger(self, record): """Returns the logger to use for this record. This implementation always return :attr:`logger`. """ return self.logger def convert_level(self, level): """Converts a logbook level into a logging level.""" if level >= logbook.CRITICAL: return logging.CRITICAL if level >= logbook.ERROR: return logging.ERROR if level >= logbook.WARNING: return logging.WARNING if level >= logbook.INFO: return logging.INFO return logging.DEBUG def convert_time(self, dt): """Converts a datetime object into a timestamp.""" year, month, day, hour, minute, second = dt.utctimetuple()[:6] days = date(year, month, 1).toordinal() - _epoch_ord + day - 1 hours = days * 24 + hour minutes = hours * 60 + minute seconds = minutes * 60 + second return seconds def convert_record(self, old_record): """Converts a record from logbook to logging.""" if sys.version_info >= (2, 5): # make sure 2to3 does not screw this up optional_kwargs = {'func': getattr(old_record, 'func_name')} else: optional_kwargs = {} record = logging.LogRecord(old_record.channel, self.convert_level(old_record.level), old_record.filename, old_record.lineno, old_record.message, (), old_record.exc_info, **optional_kwargs) for key, value in iteritems(old_record.extra): record.__dict__.setdefault(key, value) record.created = self.convert_time(old_record.time) return record def emit(self, record): self.get_logger(record).handle(self.convert_record(record)) def redirect_warnings(): """Like :func:`redirected_warnings` but will redirect all warnings to the shutdown of the interpreter: .. code-block:: python from logbook.compat import redirect_warnings redirect_warnings() """ redirected_warnings().__enter__() class redirected_warnings(object): """A context manager that copies and restores the warnings filter upon exiting the context, and logs warnings using the logbook system. The :attr:`~logbook.LogRecord.channel` attribute of the log record will be the import name of the warning. Example usage: .. code-block:: python from logbook.compat import redirected_warnings from warnings import warn with redirected_warnings(): warn(DeprecationWarning('logging should be deprecated')) """ def __init__(self): self._entered = False def message_to_unicode(self, message): try: return u(str(message)) except UnicodeError: return str(message).decode('utf-8', 'replace') def make_record(self, message, exception, filename, lineno): category = exception.__name__ if exception.__module__ not in ('exceptions', 'builtins'): category = exception.__module__ + '.' + category rv = logbook.LogRecord(category, logbook.WARNING, message) # we don't know the caller, but we get that information from the # warning system. Just attach them. rv.filename = filename rv.lineno = lineno return rv def start(self): if self._entered: # pragma: no cover raise RuntimeError("Cannot enter %r twice" % self) self._entered = True self._filters = warnings.filters warnings.filters = self._filters[:] self._showwarning = warnings.showwarning def showwarning(message, category, filename, lineno, file=None, line=None): message = self.message_to_unicode(message) record = self.make_record(message, category, filename, lineno) logbook.dispatch_record(record) warnings.showwarning = showwarning def end(self, etype=None, evalue=None, tb=None): if not self._entered: # pragma: no cover raise RuntimeError("Cannot exit %r without entering first" % self) warnings.filters = self._filters warnings.showwarning = self._showwarning __enter__ = start __exit__ = end