From dfdc68697f4c74823cc0620738acfa717c44f9aa Mon Sep 17 00:00:00 2001 From: Ned Konz Date: Wed, 18 Oct 2023 14:54:42 -0700 Subject: [PATCH] logging.py: CPython-compatible logging improvements This commit allows for logging handlers to be added to the root logger and then used by non-root loggers that don't have their own handlers. It also adds the (CPython-compatible) handlers argument to logging.basicConfig() to ease this initialization: import logging sh = logging.StreamHandler() fh = logging.FileHandler("my.log", mode="a") logging.basicConfig(handlers=[sh, fh]) root_logger = logging.getLogger() # uses sh and fh another_logger = logging.getLogger("another") # inherits handlers It also adds the Logger.removeHandler() method and avoids repeated handler addition. It also correctly calls the superclass constructor from the StreamHandler constructor and uses a default formatter if a Handler has none set (as in PR #710). Signed-off-by: Ned Konz --- python-stdlib/logging/logging.py | 41 ++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/python-stdlib/logging/logging.py b/python-stdlib/logging/logging.py index d17e42c4f..2ba0895fd 100644 --- a/python-stdlib/logging/logging.py +++ b/python-stdlib/logging/logging.py @@ -25,6 +25,7 @@ _stream = sys.stderr _default_fmt = "%(levelname)s:%(name)s:%(message)s" _default_datefmt = "%Y-%m-%d %H:%M:%S" +_default_formatter = None class LogRecord: @@ -43,6 +44,13 @@ def __init__(self, level=NOTSET): self.level = level self.formatter = None + @staticmethod + def _default_formatter(): + global _default_formatter + if _default_formatter is None: + _default_formatter = Formatter() + return _default_formatter + def close(self): pass @@ -53,11 +61,16 @@ def setFormatter(self, formatter): self.formatter = formatter def format(self, record): - return self.formatter.format(record) + if self.formatter: + fmt = self.formatter + else: + fmt = self._default_formatter() + return fmt.format(record) class StreamHandler(Handler): def __init__(self, stream=None): + super().__init__() self.stream = _stream if stream is None else stream self.terminator = "\n" @@ -128,7 +141,7 @@ def log(self, level, msg, *args): msg = msg % args self.record.set(self.name, level, msg) handlers = self.handlers - if not handlers: + if not self.hasHandlers(): handlers = getLogger().handlers for h in handlers: h.emit(self.record) @@ -161,7 +174,13 @@ def exception(self, msg, *args, exc_info=True): self.log(ERROR, buf.getvalue()) def addHandler(self, handler): - self.handlers.append(handler) + if handler not in self.handlers: + self.handlers.append(handler) + + def removeHandler(self, handler): + if handler in self.handlers: + handler.close() + self.handlers.remove(handler) def hasHandlers(self): return len(self.handlers) > 0 @@ -225,17 +244,28 @@ def basicConfig( stream=None, encoding="UTF-8", force=False, + handlers=None, ): if "root" not in _loggers: _loggers["root"] = Logger("root") logger = _loggers["root"] - if force or not logger.handlers: + if force: for h in logger.handlers: h.close() logger.handlers = [] + if len([arg for arg in (filename, stream, handlers) if arg is not None]) > 1: + raise ValueError("can only set one of 'filename', 'stream' or 'handlers'") + + if handlers is not None: + for h in handlers: + if h.formatter is None: + h.setFormatter(Formatter(format, datefmt)) + logger.addHandler(h) + + if not logger.hasHandlers(): if filename is None: handler = StreamHandler(stream) else: @@ -244,9 +274,10 @@ def basicConfig( handler.setLevel(level) handler.setFormatter(Formatter(format, datefmt)) - logger.setLevel(level) logger.addHandler(handler) + logger.setLevel(level) + if hasattr(sys, "atexit"): sys.atexit(shutdown)