mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
commit
7ccaa9a6b6
4 changed files with 49 additions and 181 deletions
|
|
@ -13,10 +13,9 @@ from collections.abc import Iterable
|
||||||
from pkgutil import extend_path
|
from pkgutil import extend_path
|
||||||
__path__ = extend_path(__path__, __name__)
|
__path__ = extend_path(__path__, __name__)
|
||||||
|
|
||||||
from rich.console import Console
|
|
||||||
|
|
||||||
# pelican.log has to be the first pelican module to be loaded
|
# pelican.log has to be the first pelican module to be loaded
|
||||||
# because logging.setLoggerClass has to be called before logging.getLogger
|
# 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.log import init as init_logging
|
||||||
from pelican.generators import (ArticlesGenerator, # noqa: I100
|
from pelican.generators import (ArticlesGenerator, # noqa: I100
|
||||||
PagesGenerator, SourceFileGenerator,
|
PagesGenerator, SourceFileGenerator,
|
||||||
|
|
@ -37,13 +36,12 @@ except Exception:
|
||||||
|
|
||||||
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
|
DEFAULT_CONFIG_NAME = 'pelicanconf.py'
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
console = Console()
|
|
||||||
|
|
||||||
|
|
||||||
class Pelican:
|
class Pelican:
|
||||||
|
|
||||||
def __init__(self, settings):
|
def __init__(self, settings):
|
||||||
"""Pelican initialisation
|
"""Pelican initialization
|
||||||
|
|
||||||
Performs some checks on the environment before doing anything else.
|
Performs some checks on the environment before doing anything else.
|
||||||
"""
|
"""
|
||||||
|
|
@ -165,15 +163,15 @@ class Pelican:
|
||||||
'draft page',
|
'draft page',
|
||||||
'draft pages')
|
'draft pages')
|
||||||
|
|
||||||
print('Done: Processed {}, {}, {}, {}, {} and {} in {:.2f} seconds.'
|
console.print('Done: Processed {}, {}, {}, {}, {} and {} in {:.2f} seconds.'
|
||||||
.format(
|
.format(
|
||||||
pluralized_articles,
|
pluralized_articles,
|
||||||
pluralized_drafts,
|
pluralized_drafts,
|
||||||
pluralized_hidden_articles,
|
pluralized_hidden_articles,
|
||||||
pluralized_pages,
|
pluralized_pages,
|
||||||
pluralized_hidden_pages,
|
pluralized_hidden_pages,
|
||||||
pluralized_draft_pages,
|
pluralized_draft_pages,
|
||||||
time.time() - start_time))
|
time.time() - start_time))
|
||||||
|
|
||||||
def _get_generator_classes(self):
|
def _get_generator_classes(self):
|
||||||
discovered_generators = [
|
discovered_generators = [
|
||||||
|
|
@ -224,32 +222,39 @@ class Pelican:
|
||||||
|
|
||||||
writer = writers[0]
|
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)
|
return writer(self.output_path, settings=self.settings)
|
||||||
|
|
||||||
|
|
||||||
class PrintSettings(argparse.Action):
|
class PrintSettings(argparse.Action):
|
||||||
def __call__(self, parser, namespace, values, option_string):
|
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:
|
if values:
|
||||||
# One or more arguments provided, so only print those settings
|
# One or more arguments provided, so only print those settings
|
||||||
for setting in values:
|
for setting in values:
|
||||||
if setting in settings:
|
if setting in settings:
|
||||||
# Only add newline between setting name and value if dict
|
# 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{}'
|
setting_format = '\n{}:\n{}'
|
||||||
else:
|
else:
|
||||||
setting_format = '\n{}: {}'
|
setting_format = '\n{}: {}'
|
||||||
print(setting_format.format(
|
console.print(setting_format.format(
|
||||||
setting,
|
setting,
|
||||||
pprint.pformat(settings[setting])))
|
pprint.pformat(settings[setting])))
|
||||||
else:
|
else:
|
||||||
print('\n{} is not a recognized setting.'.format(setting))
|
console.print('\n{} is not a recognized setting.'.format(setting))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# No argument was given to --print-settings, so print all settings
|
# No argument was given to --print-settings, so print all settings
|
||||||
pprint.pprint(settings)
|
console.print(settings)
|
||||||
|
|
||||||
parser.exit()
|
parser.exit()
|
||||||
|
|
||||||
|
|
@ -428,8 +433,8 @@ def get_instance(args):
|
||||||
|
|
||||||
|
|
||||||
def autoreload(args, excqueue=None):
|
def autoreload(args, excqueue=None):
|
||||||
print(' --- AutoReload Mode: Monitoring `content`, `theme` and'
|
console.print(' --- AutoReload Mode: Monitoring `content`, `theme` and'
|
||||||
' `settings` for changes. ---')
|
' `settings` for changes. ---')
|
||||||
pelican, settings = get_instance(args)
|
pelican, settings = get_instance(args)
|
||||||
watcher = FileSystemWatcher(args.settings, Readers, settings)
|
watcher = FileSystemWatcher(args.settings, Readers, settings)
|
||||||
sleep = False
|
sleep = False
|
||||||
|
|
@ -448,8 +453,8 @@ def autoreload(args, excqueue=None):
|
||||||
watcher.update_watchers(settings)
|
watcher.update_watchers(settings)
|
||||||
|
|
||||||
if any(modified.values()):
|
if any(modified.values()):
|
||||||
print('\n-> Modified: {}. re-generating...'.format(
|
console.print('\n-> Modified: {}. re-generating...'.format(
|
||||||
', '.join(k for k, v in modified.items() if v)))
|
', '.join(k for k, v in modified.items() if v)))
|
||||||
pelican.run()
|
pelican.run()
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|
@ -500,8 +505,8 @@ def listen(server, port, output, excqueue=None):
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
args = parse_arguments(argv)
|
args = parse_arguments(argv)
|
||||||
logs_dedup_min_level = getattr(logging, args.logs_dedup_min_level)
|
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,
|
||||||
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('Pelican version: %s', __version__)
|
||||||
logger.debug('Python version: %s', sys.version.split()[0])
|
logger.debug('Python version: %s', sys.version.split()[0])
|
||||||
|
|
@ -538,9 +543,8 @@ def main(argv=None):
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.warning('Keyboard interrupt received. Exiting.')
|
logger.warning('Keyboard interrupt received. Exiting.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical('%s', e)
|
logger.critical("%s: %s", e.__class__.__name__, e)
|
||||||
|
|
||||||
if args.verbosity == logging.DEBUG:
|
if args.verbosity == logging.DEBUG:
|
||||||
raise
|
console.print_exception()
|
||||||
else:
|
sys.exit(getattr(e, 'exitcode', 1))
|
||||||
sys.exit(getattr(e, 'exitcode', 1))
|
|
||||||
|
|
|
||||||
116
pelican/log.py
116
pelican/log.py
|
|
@ -1,80 +1,14 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Mapping
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'init'
|
'init'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
console = Console()
|
||||||
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):
|
class LimitFilter(logging.Filter):
|
||||||
|
|
@ -169,40 +103,20 @@ logging.setLoggerClass(FatalLogger)
|
||||||
logging.getLogger().__class__ = FatalLogger
|
logging.getLogger().__class__ = FatalLogger
|
||||||
|
|
||||||
|
|
||||||
def supports_color():
|
def init(level=None, fatal='', handler=RichHandler(console=console), name=None,
|
||||||
"""
|
|
||||||
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,
|
|
||||||
logs_dedup_min_level=None):
|
logs_dedup_min_level=None):
|
||||||
FatalLogger.warnings_fatal = fatal.startswith('warning')
|
FatalLogger.warnings_fatal = fatal.startswith('warning')
|
||||||
FatalLogger.errors_fatal = bool(fatal)
|
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 = logging.getLogger(name)
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
if level:
|
if level:
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
|
|
@ -218,9 +132,9 @@ def log_warnings():
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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.debug('debug')
|
||||||
root_logger.info('info')
|
root_logger.info('info')
|
||||||
root_logger.warning('warning')
|
root_logger.warning('warning')
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,10 @@ class TestLog(unittest.TestCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
self.handler = LogCountHandler()
|
self.handler = LogCountHandler()
|
||||||
self.handler.setFormatter(log.get_formatter())
|
|
||||||
self.logger.addHandler(self.handler)
|
self.logger.addHandler(self.handler)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self._reset_limit_filter()
|
self._reset_limit_filter()
|
||||||
self.logger.removeHandler(self.handler)
|
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
def _reset_limit_filter(self):
|
def _reset_limit_filter(self):
|
||||||
|
|
@ -34,54 +32,6 @@ class TestLog(unittest.TestCase):
|
||||||
self._reset_limit_filter()
|
self._reset_limit_filter()
|
||||||
self.handler.flush()
|
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 test_log_filter(self):
|
||||||
def do_logging():
|
def do_logging():
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
|
|
|
||||||
|
|
@ -827,7 +827,7 @@ class FileSystemWatcher:
|
||||||
if result.get('content') is None:
|
if result.get('content') is None:
|
||||||
reader_descs = sorted(
|
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()
|
for r in self.reader_class(self.settings).readers.values()
|
||||||
if r.enabled
|
if r.enabled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue