From 923a29aaad18fb6d6131b314e922a31bb051c7fb Mon Sep 17 00:00:00 2001 From: Peter Sabaini Date: Sat, 9 May 2020 18:15:16 +0200 Subject: [PATCH] Override settings from the command line Add a --setting-overrides KEY=VAL command line option to override default settings or those defined in settings files. This adds flexibility in running Pelican and helps reduce sprawl of settings files. Cast int and str setting overrides to their respective types. Support other setting types by treating them as JSON. Fall back to JSON when an override typecast errors. This should make it possible to set int values to None, resp. to JSON 'none' --- pelican/__init__.py | 27 ++++++++++++++++++++++++++- pelican/settings.py | 21 +++++++++++++++++++++ pelican/tests/test_settings.py | 19 +++++++++++++++++-- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/pelican/__init__.py b/pelican/__init__.py index 6469c607..9204d952 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -23,7 +23,7 @@ from pelican.plugins import signals from pelican.plugins._utils import load_plugins from pelican.readers import Readers from pelican.server import ComplexHTTPRequestHandler, RootedHTTPServer -from pelican.settings import read_settings +from pelican.settings import coerce_overrides, read_settings from pelican.utils import (FileSystemWatcher, clean_output_dir, maybe_pluralize) from pelican.writers import Writer @@ -230,6 +230,18 @@ class PrintSettings(argparse.Action): parser.exit() +class ParseDict(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + d = {} + if values: + for item in values: + split_items = item.split("=", 1) + key = split_items[0].strip() + value = split_items[1].strip() + d[key] = value + setattr(namespace, self.dest, d) + + def parse_arguments(argv=None): parser = argparse.ArgumentParser( description='A tool to generate a static blog, ' @@ -323,6 +335,18 @@ def parse_arguments(argv=None): help='IP to bind to when serving files via HTTP ' '(default: 127.0.0.1)') + parser.add_argument('-c', '--setting-overrides', dest='overrides', + help='Specify one ore more SETTING=VALUE pairs ' + '(without spaces around =) to override ' + 'settings files. If VALUE contains spaces, add quotes: ' + 'SETTING="VALUE". ' + 'Integers and strings are autoconverted, other values ' + 'can be passed in in json notation, ' + 'e.g. SETTING=none', + nargs='*', + action=ParseDict + ) + args = parser.parse_args(argv) if args.port is not None and not args.listen: @@ -358,6 +382,7 @@ def get_config(args): if args.bind is not None: config['BIND'] = args.bind config['DEBUG'] = args.verbosity == logging.DEBUG + config.update(coerce_overrides(args.overrides)) return config diff --git a/pelican/settings.py b/pelican/settings.py index 7b333de8..40804e0b 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -1,6 +1,7 @@ import copy import importlib.util import inspect +import json import locale import logging import os @@ -658,3 +659,23 @@ def configure_settings(settings): continue # setting not specified, nothing to do return settings + + +def coerce_overrides(overrides): + coerced = {} + types_to_cast = {int, str} + for k, v in overrides.items(): + if k not in overrides: + logger.warning('Override for unknown setting %s, ignoring', k) + continue + setting_type = type(DEFAULT_CONFIG[k]) + if setting_type not in types_to_cast: + coerced[k] = json.loads(v) + else: + try: + coerced[k] = setting_type(v) + except ValueError: + logger.debug('ValueError for %s override with %s, try to ' + 'load as json', k, v) + coerced[k] = json.loads(v) + return coerced diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py index 708c0981..83203ae5 100644 --- a/pelican/tests/test_settings.py +++ b/pelican/tests/test_settings.py @@ -7,8 +7,8 @@ from sys import platform from pelican.settings import (DEFAULT_CONFIG, DEFAULT_THEME, _printf_s_to_format_field, - configure_settings, handle_deprecated_settings, - read_settings) + coerce_overrides, configure_settings, + handle_deprecated_settings, read_settings) from pelican.tests.support import unittest @@ -304,3 +304,18 @@ class TestSettingsConfiguration(unittest.TestCase): [(r'C\+\+', 'cpp')] + self.settings['SLUG_REGEX_SUBSTITUTIONS']) self.assertNotIn('SLUG_SUBSTITUTIONS', settings) + + def test_coerce_overrides(self): + overrides = coerce_overrides({ + 'ARTICLE_EXCLUDES': '["testexcl"]', + 'READERS': '{"foo": "bar"}', + 'STATIC_EXCLUDE_SOURCES': 'true', + 'THEME_STATIC_DIR': 'theme', + }) + expected = { + 'ARTICLE_EXCLUDES': ["testexcl"], + 'READERS': {"foo": "bar"}, + 'STATIC_EXCLUDE_SOURCES': True, + 'THEME_STATIC_DIR': 'theme', + } + self.assertDictEqual(overrides, expected)