From 80f44c494a4d6aba21bafcd908d2f1cc930e3e87 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Wed, 30 Jun 2021 22:47:32 -0600 Subject: [PATCH 1/8] Switch to rich logging --- pelican/__init__.py | 8 +-- pelican/log.py | 115 +++++--------------------------------------- 2 files changed, 17 insertions(+), 106 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index 735002a2..d3969e29 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -43,7 +43,7 @@ console = Console() class Pelican: def __init__(self, settings): - """Pelican initialisation + """Pelican initialization Performs some checks on the environment before doing anything else. """ @@ -500,7 +500,7 @@ def listen(server, port, output, excqueue=None): def main(argv=None): args = parse_arguments(argv) logs_dedup_min_level = getattr(logging, args.logs_dedup_min_level) - init_logging(args.verbosity, args.fatal, + init_logging(level=args.verbosity, fatal=args.fatal, name=__name__, logs_dedup_min_level=logs_dedup_min_level) logger.debug('Pelican version: %s', __version__) @@ -538,9 +538,9 @@ def main(argv=None): except KeyboardInterrupt: logger.warning('Keyboard interrupt received. Exiting.') except Exception as e: - logger.critical('%s', e) + logger.critical("%s: %s" % (e.__class__.__name__, e)) if args.verbosity == logging.DEBUG: - raise + console.print_exception() else: sys.exit(getattr(e, 'exitcode', 1)) diff --git a/pelican/log.py b/pelican/log.py index 325ac3ea..111ce777 100644 --- a/pelican/log.py +++ b/pelican/log.py @@ -1,82 +1,13 @@ import logging -import os -import sys from collections import defaultdict -from collections.abc import Mapping + +from rich.logging import RichHandler __all__ = [ 'init' ] -class BaseFormatter(logging.Formatter): - def __init__(self, fmt=None, datefmt=None): - FORMAT = '%(customlevelname)s %(message)s' - super().__init__(fmt=FORMAT, datefmt=datefmt) - - def format(self, record): - customlevel = self._get_levelname(record.levelname) - record.__dict__['customlevelname'] = customlevel - # format multiline messages 'nicely' to make it clear they are together - record.msg = record.msg.replace('\n', '\n | ') - if not isinstance(record.args, Mapping): - record.args = tuple(arg.replace('\n', '\n | ') if - isinstance(arg, str) else - arg for arg in record.args) - return super().format(record) - - def formatException(self, ei): - ''' prefix traceback info for better representation ''' - s = super().formatException(ei) - # fancy format traceback - s = '\n'.join(' | ' + line for line in s.splitlines()) - # separate the traceback from the preceding lines - s = ' |___\n{}'.format(s) - return s - - def _get_levelname(self, name): - ''' NOOP: overridden by subclasses ''' - return name - - -class ANSIFormatter(BaseFormatter): - ANSI_CODES = { - 'red': '\033[1;31m', - 'yellow': '\033[1;33m', - 'cyan': '\033[1;36m', - 'white': '\033[1;37m', - 'bgred': '\033[1;41m', - 'bggrey': '\033[1;100m', - 'reset': '\033[0;m'} - - LEVEL_COLORS = { - 'INFO': 'cyan', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'bgred', - 'DEBUG': 'bggrey'} - - def _get_levelname(self, name): - color = self.ANSI_CODES[self.LEVEL_COLORS.get(name, 'white')] - if name == 'INFO': - fmt = '{0}->{2}' - else: - fmt = '{0}{1}{2}:' - return fmt.format(color, name, self.ANSI_CODES['reset']) - - -class TextFormatter(BaseFormatter): - """ - Convert a `logging.LogRecord' object into text. - """ - - def _get_levelname(self, name): - if name == 'INFO': - return '->' - else: - return name + ':' - - class LimitFilter(logging.Filter): """ Remove duplicates records, and limit the number of records in the same @@ -169,40 +100,20 @@ logging.setLoggerClass(FatalLogger) logging.getLogger().__class__ = FatalLogger -def supports_color(): - """ - Returns True if the running system's terminal supports color, - and False otherwise. - - from django.core.management.color - """ - plat = sys.platform - supported_platform = plat != 'Pocket PC' and \ - (plat != 'win32' or 'ANSICON' in os.environ) - - # isatty is not always implemented, #6223. - is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() - if not supported_platform or not is_a_tty: - return False - return True - - -def get_formatter(): - if supports_color(): - return ANSIFormatter() - else: - return TextFormatter() - - -def init(level=None, fatal='', handler=logging.StreamHandler(), name=None, +def init(level=None, fatal='', handler=RichHandler(), name=None, logs_dedup_min_level=None): FatalLogger.warnings_fatal = fatal.startswith('warning') FatalLogger.errors_fatal = bool(fatal) - logger = logging.getLogger(name) + LOG_FORMAT = "%(message)s" + logging.basicConfig( + level=level, + format=LOG_FORMAT, + datefmt="[%H:%M:%S]", + handlers=[handler] + ) - handler.setFormatter(get_formatter()) - logger.addHandler(handler) + logger = logging.getLogger(name) if level: logger.setLevel(level) @@ -218,9 +129,9 @@ def log_warnings(): if __name__ == '__main__': - init(level=logging.DEBUG) + init(level=logging.DEBUG, name=__name__) - root_logger = logging.getLogger() + root_logger = logging.getLogger(__name__) root_logger.debug('debug') root_logger.info('info') root_logger.warning('warning') From 4bfcedb8a543fa8ecd509446efb3c80e36841f6a Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Wed, 30 Jun 2021 23:03:22 -0600 Subject: [PATCH 2/8] Share rich handler between spinner and logging --- pelican/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index d3969e29..c4e2580b 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -14,6 +14,7 @@ from pkgutil import extend_path __path__ = extend_path(__path__, __name__) from rich.console import Console +from rich.logging import RichHandler # pelican.log has to be the first pelican module to be loaded # because logging.setLoggerClass has to be called before logging.getLogger @@ -500,7 +501,8 @@ def listen(server, port, output, excqueue=None): def main(argv=None): args = parse_arguments(argv) logs_dedup_min_level = getattr(logging, args.logs_dedup_min_level) - init_logging(level=args.verbosity, fatal=args.fatal, name=__name__, + init_logging(level=args.verbosity, fatal=args.fatal, + handler=RichHandler(console=console), name=__name__, logs_dedup_min_level=logs_dedup_min_level) logger.debug('Pelican version: %s', __version__) From 7d492bad67d78f4d08afa58abf1d1f83198c15fb Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Wed, 30 Jun 2021 23:29:20 -0600 Subject: [PATCH 3/8] Remove log format test as *rich* is now doing this --- pelican/tests/test_log.py | 50 --------------------------------------- 1 file changed, 50 deletions(-) diff --git a/pelican/tests/test_log.py b/pelican/tests/test_log.py index 3f9d7250..1f2fb83a 100644 --- a/pelican/tests/test_log.py +++ b/pelican/tests/test_log.py @@ -12,12 +12,10 @@ class TestLog(unittest.TestCase): super().setUp() self.logger = logging.getLogger(__name__) self.handler = LogCountHandler() - self.handler.setFormatter(log.get_formatter()) self.logger.addHandler(self.handler) def tearDown(self): self._reset_limit_filter() - self.logger.removeHandler(self.handler) super().tearDown() def _reset_limit_filter(self): @@ -34,54 +32,6 @@ class TestLog(unittest.TestCase): self._reset_limit_filter() self.handler.flush() - def test_log_formatter(self): - counter = self.handler.count_formatted_logs - with self.reset_logger(): - # log simple case - self.logger.warning('Log %s', 'test') - self.assertEqual( - counter('Log test', logging.WARNING), - 1) - - with self.reset_logger(): - # log multiline message - self.logger.warning('Log\n%s', 'test') - # Log - # | test - self.assertEqual( - counter('Log', logging.WARNING), - 1) - self.assertEqual( - counter(' | test', logging.WARNING), - 1) - - with self.reset_logger(): - # log multiline argument - self.logger.warning('Log %s', 'test1\ntest2') - # Log test1 - # | test2 - self.assertEqual( - counter('Log test1', logging.WARNING), - 1) - self.assertEqual( - counter(' | test2', logging.WARNING), - 1) - - with self.reset_logger(): - # log single list - self.logger.warning('Log %s', ['foo', 'bar']) - self.assertEqual( - counter(r"Log \['foo', 'bar'\]", logging.WARNING), - 1) - - with self.reset_logger(): - # log single dict - self.logger.warning('Log %s', {'foo': 1, 'bar': 2}) - self.assertEqual( - # dict order is not guaranteed - counter(r"Log {'.*': \d, '.*': \d}", logging.WARNING), - 1) - def test_log_filter(self): def do_logging(): for i in range(5): From a52922bfb597a4799cecff4641a5a2f9c40d99aa Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 8 Jul 2021 21:33:22 -0600 Subject: [PATCH 4/8] Move rich's console to log.py --- pelican/__init__.py | 11 +++-------- pelican/log.py | 5 ++++- pelican/utils.py | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index c4e2580b..e4ff92be 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -13,11 +13,9 @@ from collections.abc import Iterable from pkgutil import extend_path __path__ = extend_path(__path__, __name__) -from rich.console import Console -from rich.logging import RichHandler - # pelican.log has to be the first pelican module to be loaded # because logging.setLoggerClass has to be called before logging.getLogger +from pelican.log import console from pelican.log import init as init_logging from pelican.generators import (ArticlesGenerator, # noqa: I100 PagesGenerator, SourceFileGenerator, @@ -38,7 +36,6 @@ except Exception: DEFAULT_CONFIG_NAME = 'pelicanconf.py' logger = logging.getLogger(__name__) -console = Console() class Pelican: @@ -502,8 +499,7 @@ def main(argv=None): args = parse_arguments(argv) logs_dedup_min_level = getattr(logging, args.logs_dedup_min_level) init_logging(level=args.verbosity, fatal=args.fatal, - handler=RichHandler(console=console), name=__name__, - logs_dedup_min_level=logs_dedup_min_level) + name=__name__, logs_dedup_min_level=logs_dedup_min_level) logger.debug('Pelican version: %s', __version__) logger.debug('Python version: %s', sys.version.split()[0]) @@ -544,5 +540,4 @@ def main(argv=None): if args.verbosity == logging.DEBUG: console.print_exception() - else: - sys.exit(getattr(e, 'exitcode', 1)) + sys.exit(getattr(e, 'exitcode', 1)) diff --git a/pelican/log.py b/pelican/log.py index 111ce777..be176ea8 100644 --- a/pelican/log.py +++ b/pelican/log.py @@ -1,12 +1,15 @@ import logging from collections import defaultdict +from rich.console import Console from rich.logging import RichHandler __all__ = [ 'init' ] +console = Console() + class LimitFilter(logging.Filter): """ @@ -100,7 +103,7 @@ logging.setLoggerClass(FatalLogger) logging.getLogger().__class__ = FatalLogger -def init(level=None, fatal='', handler=RichHandler(), name=None, +def init(level=None, fatal='', handler=RichHandler(console=console), name=None, logs_dedup_min_level=None): FatalLogger.warnings_fatal = fatal.startswith('warning') FatalLogger.errors_fatal = bool(fatal) diff --git a/pelican/utils.py b/pelican/utils.py index 8c3a886d..e5c64470 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -811,7 +811,7 @@ class FileSystemWatcher: if result.get('content') is None: reader_descs = sorted( { - '%s (%s)' % (type(r).__name__, ', '.join(r.file_extensions)) + ' | %s (%s)' % (type(r).__name__, ', '.join(r.file_extensions)) for r in self.reader_class(self.settings).readers.values() if r.enabled } From 7eb730af78b0a5375e077c6f02f4440bc4578d48 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Tue, 6 Jul 2021 21:31:54 -0600 Subject: [PATCH 5/8] Nicer logging of found writer matches generator format --- pelican/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index e4ff92be..4014e971 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -222,7 +222,7 @@ class Pelican: writer = writers[0] - logger.debug("Found writer: %s", writer) + logger.debug("Found writer: %s (%s)" % (writer.__name__, writer.__module__)) return writer(self.output_path, settings=self.settings) From bc21922cf217c4e05f9c28fbc30349271735798e Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Fri, 9 Jul 2021 09:51:06 -0600 Subject: [PATCH 6/8] Don't preformat log messages as per review notes --- pelican/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index 4014e971..31f98e52 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -222,7 +222,7 @@ class Pelican: writer = writers[0] - logger.debug("Found writer: %s (%s)" % (writer.__name__, writer.__module__)) + logger.debug("Found writer: %s (%s)", writer.__name__, writer.__module__) return writer(self.output_path, settings=self.settings) @@ -536,7 +536,7 @@ def main(argv=None): except KeyboardInterrupt: logger.warning('Keyboard interrupt received. Exiting.') except Exception as e: - logger.critical("%s: %s" % (e.__class__.__name__, e)) + logger.critical("%s: %s", e.__class__.__name__, e) if args.verbosity == logging.DEBUG: console.print_exception() From 1cd7dd6a28998f0c003d938d68ae8b1081f6b9ac Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Fri, 9 Jul 2021 09:55:48 -0600 Subject: [PATCH 7/8] Print to the (rich) console, rather than directly --- pelican/__init__.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index 31f98e52..ebf90b79 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -163,15 +163,15 @@ class Pelican: 'draft page', 'draft pages') - print('Done: Processed {}, {}, {}, {}, {} and {} in {:.2f} seconds.' - .format( - pluralized_articles, - pluralized_drafts, - pluralized_hidden_articles, - pluralized_pages, - pluralized_hidden_pages, - pluralized_draft_pages, - time.time() - start_time)) + console.print('Done: Processed {}, {}, {}, {}, {} and {} in {:.2f} seconds.' + .format( + pluralized_articles, + pluralized_drafts, + pluralized_hidden_articles, + pluralized_pages, + pluralized_hidden_pages, + pluralized_draft_pages, + time.time() - start_time)) def _get_generator_classes(self): discovered_generators = [ @@ -426,8 +426,8 @@ def get_instance(args): def autoreload(args, excqueue=None): - print(' --- AutoReload Mode: Monitoring `content`, `theme` and' - ' `settings` for changes. ---') + console.print(' --- AutoReload Mode: Monitoring `content`, `theme` and' + ' `settings` for changes. ---') pelican, settings = get_instance(args) watcher = FileSystemWatcher(args.settings, Readers, settings) sleep = False @@ -446,8 +446,8 @@ def autoreload(args, excqueue=None): watcher.update_watchers(settings) if any(modified.values()): - print('\n-> Modified: {}. re-generating...'.format( - ', '.join(k for k, v in modified.items() if v))) + console.print('\n-> Modified: {}. re-generating...'.format( + ', '.join(k for k, v in modified.items() if v))) pelican.run() except KeyboardInterrupt: From a168470f294575655750776335da189697cec84b Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Fri, 9 Jul 2021 09:56:11 -0600 Subject: [PATCH 8/8] Use rich.console with printing settings --- pelican/__init__.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index ebf90b79..6c2c3051 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -228,26 +228,33 @@ class Pelican: class PrintSettings(argparse.Action): def __call__(self, parser, namespace, values, option_string): - instance, settings = get_instance(namespace) + init_logging(name=__name__) + + try: + instance, settings = get_instance(namespace) + except Exception as e: + logger.critical("%s: %s", e.__class__.__name__, e) + console.print_exception() + sys.exit(getattr(e, 'exitcode', 1)) if values: # One or more arguments provided, so only print those settings for setting in values: if setting in settings: # Only add newline between setting name and value if dict - if isinstance(settings[setting], dict): + if isinstance(settings[setting], (dict, tuple, list)): setting_format = '\n{}:\n{}' else: setting_format = '\n{}: {}' - print(setting_format.format( + console.print(setting_format.format( setting, pprint.pformat(settings[setting]))) else: - print('\n{} is not a recognized setting.'.format(setting)) + console.print('\n{} is not a recognized setting.'.format(setting)) break else: # No argument was given to --print-settings, so print all settings - pprint.pprint(settings) + console.print(settings) parser.exit()