Source code for logquacious.backport_configurable_stacklevel

"""
Backport of configurable stacklevel for logging added in Python 3.8.

See https://github.com/python/cpython/pull/7424
"""
import io
import logging
import os
import sys
import traceback
from contextlib import contextmanager


__all__ = ['PatchedLoggerMixin', 'patch_logger']


[docs]class PatchedLoggerMixin(object): """Mixin adding `temp_monkey_patched_logger` that allows stacklevel kwarg. Classes that include this mixin have a `temp_monkey_patched_logger` context manager that allows the use of the `stacklevel` keyword argument from Python 3.8. Classes using this mixin must have a `logging.Logger` instance as an attribute of the class. By default, this is assumed to be named `logger`, but you can override the `logger_attribute` class attribute with the name of a different attribute. """ #: Name of logger instance on the class inheriting this mixin. logger_attribute = 'logger' def __init__(self, *args, **kwargs): super(PatchedLoggerMixin, self).__init__(*args, **kwargs) self._patched_logger_class = None def _get_logger(self): if not hasattr(self, self.logger_attribute): msg = ( "Subclass of PatchedLoggerMixin must define `{}` attribute " "or override `logger_attribute`".format(self.logger_attribute) ) raise AttributeError(msg) return getattr(self, self.logger_attribute)
[docs] @contextmanager def temp_monkey_patched_logger(self): """Temporarily monkey patch logger to allow overriding log records. The monkey patching is reset so that the behavior change is limited to the scope of this logger. """ logger = self._get_logger() original_logger_class = logger.__class__ # Cache patched logger class if not already defined. if self._patched_logger_class is None: self._patched_logger_class = patch_logger(logger.__class__) logger.__class__ = self._patched_logger_class try: yield finally: logger.__class__ = original_logger_class
[docs]def patch_logger(logger_class): """Return logger class patched with stacklevel keyword argument.""" if sys.version_info.major >= 3 and sys.version_info.minor >= 8: return logger_class return type('ConfigurableStacklevelLogger', (ConfigurableStacklevelLoggerMixin, logger_class), {})
class ConfigurableStacklevelLoggerMixin(object): """Mixin for adding `stacklevel` keyword argument for logging methods. This mixin can be used to monkey patch `logging.Logger` to include the `stacklevel` keyword argument that will be available in Python 3.8. See https://github.com/python/cpython/pull/7424 """ def findCaller(self, stack_info=False, stacklevel=1): # pragma: no cover """ Find the stack frame of the caller so that we can note the source file name, line number and function name. """ f = logging.currentframe() # On some versions of IronPython, currentframe() returns None if # IronPython isn't run with -X:Frames. if f is not None: f = f.f_back orig_f = f while f and stacklevel > 1: f = f.f_back stacklevel -= 1 if not f: f = orig_f rv = "(unknown file)", 0, "(unknown function)", None while hasattr(f, "f_code"): co = f.f_code filename = os.path.normcase(co.co_filename) if filename == logging._srcfile: f = f.f_back continue sinfo = None if stack_info: sio = io.StringIO() sio.write('Stack (most recent call last):\n') traceback.print_stack(f, file=sio) sinfo = sio.getvalue() if sinfo[-1] == '\n': sinfo = sinfo[:-1] sio.close() rv = (co.co_filename, f.f_lineno, co.co_name, sinfo) break return rv def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, stacklevel=1): # pragma: no cover """ Low-level logging routine which creates a LogRecord and then calls all the handlers of this logger to handle the record. """ sinfo = None if logging._srcfile: # IronPython doesn't track Python frames, so findCaller raises an # exception on some versions of IronPython. We trap it here so that # IronPython can use logging. try: fn, lno, func, sinfo = self.findCaller(stack_info, stacklevel) except ValueError: fn, lno, func = "(unknown file)", 0, "(unknown function)" else: fn, lno, func = "(unknown file)", 0, "(unknown function)" if exc_info: if isinstance(exc_info, BaseException): exc_info = (type(exc_info), exc_info, exc_info.__traceback__) elif not isinstance(exc_info, tuple): exc_info = sys.exc_info() if sys.version_info.major >= 3: record = self.makeRecord(self.name, level, fn, lno, msg, args, exc_info, func, extra, sinfo) else: record = self.makeRecord(self.name, level, fn, lno, msg, args, exc_info, func, extra) self.handle(record)