From d717b04cb32f6badb7de10667b0b9525c59ae087 Mon Sep 17 00:00:00 2001 From: iabdalkader Date: Sat, 16 Jul 2022 17:33:39 +0200 Subject: [PATCH] logging: Improve the logging module. Add support for all format specifiers, support for `datefmt` using (optional) strftime, and support for Stream and File handlers. Ports/boards that need to use `FileHandlers` should enable `MICROPY_PY_SYS_ATEXIT`, and enabled `MICROPY_PY_SYS_EXC_INFO` if using `logging.exception()`. --- .../logging/examples/basic_example.py | 3 + .../example_logging_1.py} | 0 .../logging/examples/example_logging_2.py | 47 ++++ python-stdlib/logging/examples/root_logger.py | 7 + python-stdlib/logging/logging.py | 236 +++++++++++++----- python-stdlib/logging/manifest.py | 2 +- 6 files changed, 233 insertions(+), 62 deletions(-) create mode 100644 python-stdlib/logging/examples/basic_example.py rename python-stdlib/logging/{example_logging.py => examples/example_logging_1.py} (100%) create mode 100644 python-stdlib/logging/examples/example_logging_2.py create mode 100644 python-stdlib/logging/examples/root_logger.py diff --git a/python-stdlib/logging/examples/basic_example.py b/python-stdlib/logging/examples/basic_example.py new file mode 100644 index 00000000..92dd9bb9 --- /dev/null +++ b/python-stdlib/logging/examples/basic_example.py @@ -0,0 +1,3 @@ +import logging + +logging.warning("test") diff --git a/python-stdlib/logging/example_logging.py b/python-stdlib/logging/examples/example_logging_1.py similarity index 100% rename from python-stdlib/logging/example_logging.py rename to python-stdlib/logging/examples/example_logging_1.py diff --git a/python-stdlib/logging/examples/example_logging_2.py b/python-stdlib/logging/examples/example_logging_2.py new file mode 100644 index 00000000..a349d13b --- /dev/null +++ b/python-stdlib/logging/examples/example_logging_2.py @@ -0,0 +1,47 @@ +import logging + +# Create logger +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Create console handler and set level to debug +stream_handler = logging.StreamHandler() +stream_handler.setLevel(logging.DEBUG) + +# Create file handler and set level to error +file_handler = logging.FileHandler("error.log", mode="w") +file_handler.setLevel(logging.ERROR) + +# Create a formatter +formatter = logging.Formatter("%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(message)s") + +# Add formatter to the handlers +stream_handler.setFormatter(formatter) +file_handler.setFormatter(formatter) + +# Add handlers to logger +logger.addHandler(stream_handler) +logger.addHandler(file_handler) + +# Log some messages +logger.debug("debug message") +logger.info("info message") +logger.warning("warn message") +logger.error("error message") +logger.critical("critical message") +logger.info("message %s %d", "arg", 5) +logger.info("message %(foo)s %(bar)s", {"foo": 1, "bar": 20}) + +try: + 1 / 0 +except: + logger.error("Some trouble (%s)", "expected") + +# Custom handler example +class MyHandler(logging.Handler): + def emit(self, record): + print("levelname=%(levelname)s name=%(name)s message=%(message)s" % record.__dict__) + + +logging.getLogger().addHandler(MyHandler()) +logging.info("Test message7") diff --git a/python-stdlib/logging/examples/root_logger.py b/python-stdlib/logging/examples/root_logger.py new file mode 100644 index 00000000..47e7216d --- /dev/null +++ b/python-stdlib/logging/examples/root_logger.py @@ -0,0 +1,7 @@ +import logging, sys + +logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) +for handler in logging.getLogger().handlers: + handler.setFormatter(logging.Formatter("[%(levelname)s]:%(name)s:%(message)s")) +logging.info("hello upy") +logging.getLogger("child").info("hello 2") diff --git a/python-stdlib/logging/logging.py b/python-stdlib/logging/logging.py index 79bd61c7..ed2dadec 100644 --- a/python-stdlib/logging/logging.py +++ b/python-stdlib/logging/logging.py @@ -1,4 +1,8 @@ import sys +import time + +if hasattr(time, "strftime"): + from time import strftime CRITICAL = 50 ERROR = 40 @@ -8,46 +12,104 @@ DEBUG = 10 NOTSET = 0 _level_dict = { - CRITICAL: "CRIT", + CRITICAL: "CRITICAL", ERROR: "ERROR", - WARNING: "WARN", + WARNING: "WARNING", INFO: "INFO", DEBUG: "DEBUG", + NOTSET: "NOTSET", } +_loggers = {} _stream = sys.stderr +_level = INFO +_default_fmt = "%(levelname)s:%(name)s:%(message)s" +_default_datefmt = "%Y-%m-%d %H:%M:%S" class LogRecord: - def __init__(self): - self.__dict__ = {} - - def __getattr__(self, key): - return self.__dict__[key] + def set(self, name, level, message): + self.name = name + self.levelno = level + self.levelname = _level_dict[level] + self.message = message + self.ct = time.time() + self.msecs = int((self.ct - int(self.ct)) * 1000) + self.asctime = None class Handler: - def __init__(self): + def __init__(self, level=NOTSET): + self.level = level + self.formatter = None + + def close(self): pass - def setFormatter(self, fmtr): - pass + def setLevel(self, level): + self.level = level + + def setFormatter(self, formatter): + self.formatter = formatter + + def format(self, record): + return self.formatter.format(record) + + +class StreamHandler(Handler): + def __init__(self, stream=None): + self.stream = _stream if stream is None else stream + self.terminator = "\n" + + def close(self): + if hasattr(self.stream, "flush"): + self.stream.flush() + + def emit(self, record): + if record.levelno >= self.level: + self.stream.write(self.format(record) + self.terminator) + + +class FileHandler(StreamHandler): + def __init__(self, filename, mode="a", encoding="UTF-8"): + super().__init__(stream=open(filename, mode=mode, encoding=encoding)) + + def close(self): + super().close() + self.stream.close() + + +class Formatter: + def __init__(self, fmt=None, datefmt=None): + self.fmt = _default_fmt if fmt is None else fmt + self.datefmt = _default_datefmt if datefmt is None else datefmt + + def usesTime(self): + return "asctime" in self.fmt + + def formatTime(self, datefmt, record): + if hasattr(time, "strftime"): + return strftime(datefmt, time.localtime(record.ct)) + return None + + def format(self, record): + if self.usesTime(): + record.asctime = self.formatTime(self.datefmt, record) + return self.fmt % { + "name": record.name, + "message": record.message, + "msecs": record.msecs, + "asctime": record.asctime, + "levelname": record.levelname, + } class Logger: - - level = NOTSET - handlers = [] - record = LogRecord() - - def __init__(self, name): + def __init__(self, name, level=NOTSET): self.name = name - - def _level_str(self, level): - l = _level_dict.get(level) - if l is not None: - return l - return "LVL%s" % level + self.level = level + self.handlers = [] + self.record = LogRecord() def setLevel(self, level): self.level = level @@ -57,19 +119,16 @@ class Logger: def log(self, level, msg, *args): if self.isEnabledFor(level): - levelname = self._level_str(level) if args: + if isinstance(args[0], dict): + args = args[0] msg = msg % args - if self.handlers: - d = self.record.__dict__ - d["levelname"] = levelname - d["levelno"] = level - d["message"] = msg - d["name"] = self.name - for h in self.handlers: - h.emit(self.record) - else: - print(levelname, ":", self.name, ":", msg, sep="", file=_stream) + self.record.set(self.name, level, msg) + handlers = self.handlers + if not handlers: + handlers = getLogger().handlers + for h in handlers: + h.emit(self.record) def debug(self, msg, *args): self.log(DEBUG, msg, *args) @@ -86,43 +145,98 @@ class Logger: def critical(self, msg, *args): self.log(CRITICAL, msg, *args) - def exc(self, e, msg, *args): - self.log(ERROR, msg, *args) - sys.print_exception(e, _stream) - def exception(self, msg, *args): - self.exc(sys.exc_info()[1], msg, *args) + self.log(ERROR, msg, *args) + if hasattr(sys, "exc_info"): + sys.print_exception(sys.exc_info()[1], _stream) - def addHandler(self, hndlr): - self.handlers.append(hndlr) + def addHandler(self, handler): + self.handlers.append(handler) + + def hasHandlers(self): + return len(self.handlers) > 0 -_level = INFO -_loggers = {} +def getLogger(name=None): + if name is None: + name = "root" + if name not in _loggers: + _loggers[name] = Logger(name) + if name == "root": + basicConfig() + return _loggers[name] -def getLogger(name="root"): - if name in _loggers: - return _loggers[name] - l = Logger(name) - _loggers[name] = l - return l - - -def info(msg, *args): - getLogger().info(msg, *args) +def log(level, msg, *args): + getLogger().log(level, msg, *args) def debug(msg, *args): getLogger().debug(msg, *args) -def basicConfig(level=INFO, filename=None, stream=None, format=None): - global _level, _stream - _level = level - if stream: - _stream = stream - if filename is not None: - print("logging.basicConfig: filename arg is not supported") - if format is not None: - print("logging.basicConfig: format arg is not supported") +def info(msg, *args): + getLogger().info(msg, *args) + + +def warning(msg, *args): + getLogger().warning(msg, *args) + + +def error(msg, *args): + getLogger().error(msg, *args) + + +def critical(msg, *args): + getLogger().critical(msg, *args) + + +def exception(msg, *args): + getLogger().exception(msg, *args) + + +def shutdown(): + for k, logger in _loggers.items(): + for h in logger.handlers: + h.close() + _loggers.pop(logger, None) + + +def addLevelName(level, name): + _level_dict[level] = name + + +def basicConfig( + filename=None, + filemode="a", + format=None, + datefmt=None, + level=WARNING, + stream=None, + encoding="UTF-8", + force=False, +): + if "root" not in _loggers: + _loggers["root"] = Logger("root") + + logger = _loggers["root"] + + if force or not logger.handlers: + for h in logger.handlers: + h.close() + logger.handlers = [] + + if filename is None: + handler = StreamHandler(stream) + else: + handler = FileHandler(filename, filemode, encoding) + + handler.setLevel(level) + handler.setFormatter(Formatter(format, datefmt)) + + logger.setLevel(level) + logger.addHandler(handler) + + +if hasattr(sys, "atexit"): + sys.atexit(shutdown) diff --git a/python-stdlib/logging/manifest.py b/python-stdlib/logging/manifest.py index 17601509..735434a8 100644 --- a/python-stdlib/logging/manifest.py +++ b/python-stdlib/logging/manifest.py @@ -1,3 +1,3 @@ -metadata(version="0.3") +metadata(version="0.4") module("logging.py")