From 94a38ab297361b5c0ff9f847cb9e94f37aa6a29e Mon Sep 17 00:00:00 2001 From: Ned Konz Date: Thu, 15 Feb 2024 10:19:24 -0800 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 adds the flush() method to StreamHandler and its subclasses. 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 | 46 +++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/python-stdlib/logging/logging.py b/python-stdlib/logging/logging.py index d17e42c4..378ef487 100644 --- a/python-stdlib/logging/logging.py +++ b/python-stdlib/logging/logging.py @@ -25,6 +25,7 @@ _loggers = {} _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,9 +44,19 @@ class Handler: 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 + def flush(self): + pass + def setLevel(self, level): self.level = level @@ -53,15 +64,20 @@ class Handler: 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" - def close(self): + def flush(self): if hasattr(self.stream, "flush"): self.stream.flush() @@ -128,7 +144,7 @@ class Logger: 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 +177,13 @@ class Logger: 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 +247,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 +277,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)