From 8212c2f848e98a4da83ced4dcc2b65375c28d375 Mon Sep 17 00:00:00 2001 From: "T. Joseph Carter" Date: Tue, 18 Jul 2017 09:17:31 -0700 Subject: [PATCH] Mostly style improvements to logging.py Our changes to the built-in logging module of Python are kind of a hack designed to be as lightweight as possible way to replace the built-in logging module with one that operates using newer str.format based string expansion. It's not really complete we probably should change that at some point. Changes include: - Docstrings, lots of docstrings - Type hinting - log is now LOG - pylint warnings disabled on things that will not change and are on purpose - StyleAdapter.log does not dedent msg anymore unless dedent=True is passed which hopefully should make it a little less DWIM. --- blocksfree/logging.py | 90 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/blocksfree/logging.py b/blocksfree/logging.py index 06c5b96..262ac26 100644 --- a/blocksfree/logging.py +++ b/blocksfree/logging.py @@ -16,37 +16,95 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +"""BlocksFree LoggingAdapter using str.format and textwrap.dedent + +Traditionally Python has used a % operator on strings to perform a somewhat +printf-like string formatting operation. It has limitations and str.format was +made to replace it. The old way isn't deprecated yet, and it won't be removed +any time soon, but str.format is how new code should work. + +The issue is that Python's logging module is used by old and new code alike, +and so calls to the basic logger require the use of %-style format strings in +order to work with new and old code alike, forcing you to use the less flexible +"old way". + +There isn't a perfectly clean solution to this--either you need ugliness in +each logging call, or you need a bit different ugliness up front to hide the +mess. + +Details are at the bottom of this section of the Logging Cookbook: + +https://docs.python.org/howto/logging-cookbook.html\ +#use-of-alternative-formatting-styles + +The next issue was that multi-line strings either break code indentation flow, +or they have to deal with indentation. If you don't have a special case for +the first line, textwrap.dedent works great for that. In order to avoid the +special case, just use a line continuation immediately after your opening +quotes. Another imperfect solution, but it does the job. +""" + import sys import logging import textwrap +from typing import List, Dict -### LOGGING -# *sigh* No clean/simple way to use str.format() type log strings without -# jumping through a few hoops - +# pylint: disable=too-few-public-methods,missing-docstring class Message(object): - def __init__(self, fmt, args): + def __init__(self, fmt: str, args: List) -> None: self.fmt = fmt self.args = args - def __str__(self): + def __str__(self) -> str: return self.fmt.format(*self.args) +# pylint: enable=too-few-public-methods,missing-docstring class StyleAdapter(logging.LoggerAdapter): - def __init__(self, logger, extra=None): + """Return a LoggerAdapter that uses str.format expansions in log messages + + StyleAdapter wraps the standard logger (e.g. logging.getLogger) so that it + appears to take str.format strings rather than the classic str % tuple + strings. + + Args: + logger: A logging.Logger instance/logging channel + extra: A context object (see the Python logging cookbook) + """ + + def __init__(self, logger: logging.Logger, extra=None) -> None: super(StyleAdapter, self).__init__(logger, extra or {}) - def log(self, level, msg, *args, **kwargs): + def log(self, level: int, msg: str, *args: List, **kwargs: Dict) -> None: + """Logs msg.format(*args) at the given level + + Effectively functions as if we were subclassing logging.Logger's log + method to change arg convention from msg % args to msg.format(*args). + See the documentation for the standard logging module for more info. + + Additionally can perform textwrap.dedent() on msg before logging if + dedent=True. + + Args: + level: Integer logging level + msg: A log message in the form of a str.format format string + dedent: Whether to dedent format string + args/kwargs: Positional and keyword arguments passed to _log + """ if self.isEnabledFor(level): - msg, kwargs = self.process(textwrap.dedent(msg), kwargs) + if kwargs.get('dedent', False): + msg = textwrap.dedent(msg) + msg, kwargs = self.process(msg, kwargs) + # pylint: disable=protected-access self.logger._log(level, Message(str(msg), args), (), **kwargs) + # pylint: enable=protected-access -log = StyleAdapter(logging.getLogger(__name__)) - +LOG = StyleAdapter(logging.getLogger(__name__)) # Set up our logging facility -_handler = logging.StreamHandler(sys.stdout) -_formatter = logging.Formatter('%(message)s') -_handler.setFormatter(_formatter) -log.logger.addHandler(_handler) -log.setLevel(logging.DEBUG) +# FIXME(tjcarter): get rid of log, let caller handle where it's going +log = LOG +_HANDLER = logging.StreamHandler(sys.stdout) +_FORMATTER = logging.Formatter('{message}', style='{') +_HANDLER.setFormatter(_FORMATTER) +LOG.logger.addHandler(_HANDLER) +LOG.setLevel(logging.DEBUG)