forked from github/pelican
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'
321 lines
13 KiB
Python
321 lines
13 KiB
Python
import copy
|
|
import locale
|
|
import os
|
|
from os.path import abspath, dirname, join
|
|
from sys import platform
|
|
|
|
|
|
from pelican.settings import (DEFAULT_CONFIG, DEFAULT_THEME,
|
|
_printf_s_to_format_field,
|
|
coerce_overrides, configure_settings,
|
|
handle_deprecated_settings, read_settings)
|
|
from pelican.tests.support import unittest
|
|
|
|
|
|
class TestSettingsConfiguration(unittest.TestCase):
|
|
"""Provided a file, it should read it, replace the default values,
|
|
append new values to the settings (if any), and apply basic settings
|
|
optimizations.
|
|
"""
|
|
def setUp(self):
|
|
self.old_locale = locale.setlocale(locale.LC_ALL)
|
|
locale.setlocale(locale.LC_ALL, 'C')
|
|
self.PATH = abspath(dirname(__file__))
|
|
default_conf = join(self.PATH, 'default_conf.py')
|
|
self.settings = read_settings(default_conf)
|
|
|
|
def tearDown(self):
|
|
locale.setlocale(locale.LC_ALL, self.old_locale)
|
|
|
|
def test_overwrite_existing_settings(self):
|
|
self.assertEqual(self.settings.get('SITENAME'), "Alexis' log")
|
|
self.assertEqual(
|
|
self.settings.get('SITEURL'),
|
|
'http://blog.notmyidea.org')
|
|
|
|
def test_keep_default_settings(self):
|
|
# Keep default settings if not defined.
|
|
self.assertEqual(
|
|
self.settings.get('DEFAULT_CATEGORY'),
|
|
DEFAULT_CONFIG['DEFAULT_CATEGORY'])
|
|
|
|
def test_dont_copy_small_keys(self):
|
|
# Do not copy keys not in caps.
|
|
self.assertNotIn('foobar', self.settings)
|
|
|
|
def test_read_empty_settings(self):
|
|
# Ensure an empty settings file results in default settings.
|
|
settings = read_settings(None)
|
|
expected = copy.deepcopy(DEFAULT_CONFIG)
|
|
# Added by configure settings
|
|
expected['FEED_DOMAIN'] = ''
|
|
expected['ARTICLE_EXCLUDES'] = ['pages']
|
|
expected['PAGE_EXCLUDES'] = ['']
|
|
self.maxDiff = None
|
|
self.assertDictEqual(settings, expected)
|
|
|
|
def test_settings_return_independent(self):
|
|
# Make sure that the results from one settings call doesn't
|
|
# effect past or future instances.
|
|
self.PATH = abspath(dirname(__file__))
|
|
default_conf = join(self.PATH, 'default_conf.py')
|
|
settings = read_settings(default_conf)
|
|
settings['SITEURL'] = 'new-value'
|
|
new_settings = read_settings(default_conf)
|
|
self.assertNotEqual(new_settings['SITEURL'], settings['SITEURL'])
|
|
|
|
def test_defaults_not_overwritten(self):
|
|
# This assumes 'SITENAME': 'A Pelican Blog'
|
|
settings = read_settings(None)
|
|
settings['SITENAME'] = 'Not a Pelican Blog'
|
|
self.assertNotEqual(settings['SITENAME'], DEFAULT_CONFIG['SITENAME'])
|
|
|
|
def test_static_path_settings_safety(self):
|
|
# Disallow static paths from being strings
|
|
settings = {
|
|
'STATIC_PATHS': 'foo/bar',
|
|
'THEME_STATIC_PATHS': 'bar/baz',
|
|
# These 4 settings are required to run configure_settings
|
|
'PATH': '.',
|
|
'THEME': DEFAULT_THEME,
|
|
'SITEURL': 'http://blog.notmyidea.org/',
|
|
'LOCALE': '',
|
|
}
|
|
configure_settings(settings)
|
|
self.assertEqual(
|
|
settings['STATIC_PATHS'],
|
|
DEFAULT_CONFIG['STATIC_PATHS'])
|
|
self.assertEqual(
|
|
settings['THEME_STATIC_PATHS'],
|
|
DEFAULT_CONFIG['THEME_STATIC_PATHS'])
|
|
|
|
def test_configure_settings(self):
|
|
# Manipulations to settings should be applied correctly.
|
|
settings = {
|
|
'SITEURL': 'http://blog.notmyidea.org/',
|
|
'LOCALE': '',
|
|
'PATH': os.curdir,
|
|
'THEME': DEFAULT_THEME,
|
|
}
|
|
configure_settings(settings)
|
|
|
|
# SITEURL should not have a trailing slash
|
|
self.assertEqual(settings['SITEURL'], 'http://blog.notmyidea.org')
|
|
|
|
# FEED_DOMAIN, if undefined, should default to SITEURL
|
|
self.assertEqual(settings['FEED_DOMAIN'], 'http://blog.notmyidea.org')
|
|
|
|
settings['FEED_DOMAIN'] = 'http://feeds.example.com'
|
|
configure_settings(settings)
|
|
self.assertEqual(settings['FEED_DOMAIN'], 'http://feeds.example.com')
|
|
|
|
def test_theme_settings_exceptions(self):
|
|
settings = self.settings
|
|
|
|
# Check that theme lookup in "pelican/themes" functions as expected
|
|
settings['THEME'] = os.path.split(settings['THEME'])[1]
|
|
configure_settings(settings)
|
|
self.assertEqual(settings['THEME'], DEFAULT_THEME)
|
|
|
|
# Check that non-existent theme raises exception
|
|
settings['THEME'] = 'foo'
|
|
self.assertRaises(Exception, configure_settings, settings)
|
|
|
|
def test_deprecated_dir_setting(self):
|
|
settings = self.settings
|
|
|
|
settings['ARTICLE_DIR'] = 'foo'
|
|
settings['PAGE_DIR'] = 'bar'
|
|
|
|
settings = handle_deprecated_settings(settings)
|
|
|
|
self.assertEqual(settings['ARTICLE_PATHS'], ['foo'])
|
|
self.assertEqual(settings['PAGE_PATHS'], ['bar'])
|
|
|
|
with self.assertRaises(KeyError):
|
|
settings['ARTICLE_DIR']
|
|
settings['PAGE_DIR']
|
|
|
|
# locale.getdefaultlocale() is broken on Windows
|
|
# See: https://bugs.python.org/issue37945
|
|
@unittest.skipIf(platform == 'win32', "Doesn't work on Windows")
|
|
def test_default_encoding(self):
|
|
# Test that the default locale is set if not specified in settings
|
|
|
|
# Reset locale to Python's default locale
|
|
locale.setlocale(locale.LC_ALL, 'C')
|
|
self.assertEqual(self.settings['LOCALE'], DEFAULT_CONFIG['LOCALE'])
|
|
|
|
configure_settings(self.settings)
|
|
self.assertEqual(locale.getlocale(), locale.getdefaultlocale())
|
|
|
|
def test_invalid_settings_throw_exception(self):
|
|
# Test that the path name is valid
|
|
|
|
# test that 'PATH' is set
|
|
settings = {
|
|
}
|
|
|
|
self.assertRaises(Exception, configure_settings, settings)
|
|
|
|
# Test that 'PATH' is valid
|
|
settings['PATH'] = ''
|
|
self.assertRaises(Exception, configure_settings, settings)
|
|
|
|
# Test nonexistent THEME
|
|
settings['PATH'] = os.curdir
|
|
settings['THEME'] = 'foo'
|
|
|
|
self.assertRaises(Exception, configure_settings, settings)
|
|
|
|
def test__printf_s_to_format_field(self):
|
|
for s in ('%s', '{%s}', '{%s'):
|
|
option = 'foo/{}/bar.baz'.format(s)
|
|
result = _printf_s_to_format_field(option, 'slug')
|
|
expected = option % 'qux'
|
|
found = result.format(slug='qux')
|
|
self.assertEqual(expected, found)
|
|
|
|
def test_deprecated_extra_templates_paths(self):
|
|
settings = self.settings
|
|
settings['EXTRA_TEMPLATES_PATHS'] = ['/foo/bar', '/ha']
|
|
|
|
settings = handle_deprecated_settings(settings)
|
|
|
|
self.assertEqual(settings['THEME_TEMPLATES_OVERRIDES'],
|
|
['/foo/bar', '/ha'])
|
|
self.assertNotIn('EXTRA_TEMPLATES_PATHS', settings)
|
|
|
|
def test_deprecated_paginated_direct_templates(self):
|
|
settings = self.settings
|
|
settings['PAGINATED_DIRECT_TEMPLATES'] = ['index', 'archives']
|
|
settings['PAGINATED_TEMPLATES'] = {'index': 10, 'category': None}
|
|
settings = handle_deprecated_settings(settings)
|
|
self.assertEqual(settings['PAGINATED_TEMPLATES'],
|
|
{'index': 10, 'category': None, 'archives': None})
|
|
self.assertNotIn('PAGINATED_DIRECT_TEMPLATES', settings)
|
|
|
|
def test_deprecated_paginated_direct_templates_from_file(self):
|
|
# This is equivalent to reading a settings file that has
|
|
# PAGINATED_DIRECT_TEMPLATES defined but no PAGINATED_TEMPLATES.
|
|
settings = read_settings(None, override={
|
|
'PAGINATED_DIRECT_TEMPLATES': ['index', 'archives']
|
|
})
|
|
self.assertEqual(settings['PAGINATED_TEMPLATES'], {
|
|
'archives': None,
|
|
'author': None,
|
|
'index': None,
|
|
'category': None,
|
|
'tag': None})
|
|
self.assertNotIn('PAGINATED_DIRECT_TEMPLATES', settings)
|
|
|
|
def test_theme_and_extra_templates_exception(self):
|
|
settings = self.settings
|
|
settings['EXTRA_TEMPLATES_PATHS'] = ['/ha']
|
|
settings['THEME_TEMPLATES_OVERRIDES'] = ['/foo/bar']
|
|
|
|
self.assertRaises(Exception, handle_deprecated_settings, settings)
|
|
|
|
def test_slug_and_slug_regex_substitutions_exception(self):
|
|
settings = {}
|
|
settings['SLUG_REGEX_SUBSTITUTIONS'] = [('C++', 'cpp')]
|
|
settings['TAG_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
|
|
|
self.assertRaises(Exception, handle_deprecated_settings, settings)
|
|
|
|
def test_deprecated_slug_substitutions(self):
|
|
default_slug_regex_subs = self.settings['SLUG_REGEX_SUBSTITUTIONS']
|
|
|
|
# If no deprecated setting is set, don't set new ones
|
|
settings = {}
|
|
settings = handle_deprecated_settings(settings)
|
|
self.assertNotIn('SLUG_REGEX_SUBSTITUTIONS', settings)
|
|
self.assertNotIn('TAG_REGEX_SUBSTITUTIONS', settings)
|
|
self.assertNotIn('CATEGORY_REGEX_SUBSTITUTIONS', settings)
|
|
self.assertNotIn('AUTHOR_REGEX_SUBSTITUTIONS', settings)
|
|
|
|
# If SLUG_SUBSTITUTIONS is set, set {SLUG, AUTHOR}_REGEX_SUBSTITUTIONS
|
|
# correctly, don't set {CATEGORY, TAG}_REGEX_SUBSTITUTIONS
|
|
settings = {}
|
|
settings['SLUG_SUBSTITUTIONS'] = [('C++', 'cpp')]
|
|
settings = handle_deprecated_settings(settings)
|
|
self.assertEqual(settings.get('SLUG_REGEX_SUBSTITUTIONS'),
|
|
[(r'C\+\+', 'cpp')] + default_slug_regex_subs)
|
|
self.assertNotIn('TAG_REGEX_SUBSTITUTIONS', settings)
|
|
self.assertNotIn('CATEGORY_REGEX_SUBSTITUTIONS', settings)
|
|
self.assertEqual(settings.get('AUTHOR_REGEX_SUBSTITUTIONS'),
|
|
default_slug_regex_subs)
|
|
|
|
# If {CATEGORY, TAG, AUTHOR}_SUBSTITUTIONS are set, set
|
|
# {CATEGORY, TAG, AUTHOR}_REGEX_SUBSTITUTIONS correctly, don't set
|
|
# SLUG_REGEX_SUBSTITUTIONS
|
|
settings = {}
|
|
settings['TAG_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
|
settings['CATEGORY_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
|
settings['AUTHOR_SUBSTITUTIONS'] = [('Alexander Todorov', 'atodorov')]
|
|
settings = handle_deprecated_settings(settings)
|
|
self.assertNotIn('SLUG_REGEX_SUBSTITUTIONS', settings)
|
|
self.assertEqual(settings['TAG_REGEX_SUBSTITUTIONS'],
|
|
[(r'C\#', 'csharp')] + default_slug_regex_subs)
|
|
self.assertEqual(settings['CATEGORY_REGEX_SUBSTITUTIONS'],
|
|
[(r'C\#', 'csharp')] + default_slug_regex_subs)
|
|
self.assertEqual(settings['AUTHOR_REGEX_SUBSTITUTIONS'],
|
|
[(r'Alexander\ Todorov', 'atodorov')] +
|
|
default_slug_regex_subs)
|
|
|
|
# If {SLUG, CATEGORY, TAG, AUTHOR}_SUBSTITUTIONS are set, set
|
|
# {SLUG, CATEGORY, TAG, AUTHOR}_REGEX_SUBSTITUTIONS correctly
|
|
settings = {}
|
|
settings['SLUG_SUBSTITUTIONS'] = [('C++', 'cpp')]
|
|
settings['TAG_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
|
settings['CATEGORY_SUBSTITUTIONS'] = [('C#', 'csharp')]
|
|
settings['AUTHOR_SUBSTITUTIONS'] = [('Alexander Todorov', 'atodorov')]
|
|
settings = handle_deprecated_settings(settings)
|
|
self.assertEqual(settings['TAG_REGEX_SUBSTITUTIONS'],
|
|
[(r'C\+\+', 'cpp')] + [(r'C\#', 'csharp')] +
|
|
default_slug_regex_subs)
|
|
self.assertEqual(settings['CATEGORY_REGEX_SUBSTITUTIONS'],
|
|
[(r'C\+\+', 'cpp')] + [(r'C\#', 'csharp')] +
|
|
default_slug_regex_subs)
|
|
self.assertEqual(settings['AUTHOR_REGEX_SUBSTITUTIONS'],
|
|
[(r'Alexander\ Todorov', 'atodorov')] +
|
|
default_slug_regex_subs)
|
|
|
|
# Handle old 'skip' flags correctly
|
|
settings = {}
|
|
settings['SLUG_SUBSTITUTIONS'] = [('C++', 'cpp', True)]
|
|
settings['AUTHOR_SUBSTITUTIONS'] = [('Alexander Todorov', 'atodorov',
|
|
False)]
|
|
settings = handle_deprecated_settings(settings)
|
|
self.assertEqual(settings.get('SLUG_REGEX_SUBSTITUTIONS'),
|
|
[(r'C\+\+', 'cpp')] +
|
|
[(r'(?u)\A\s*', ''), (r'(?u)\s*\Z', '')])
|
|
self.assertEqual(settings['AUTHOR_REGEX_SUBSTITUTIONS'],
|
|
[(r'Alexander\ Todorov', 'atodorov')] +
|
|
default_slug_regex_subs)
|
|
|
|
def test_deprecated_slug_substitutions_from_file(self):
|
|
# This is equivalent to reading a settings file that has
|
|
# SLUG_SUBSTITUTIONS defined but no SLUG_REGEX_SUBSTITUTIONS.
|
|
settings = read_settings(None, override={
|
|
'SLUG_SUBSTITUTIONS': [('C++', 'cpp')]
|
|
})
|
|
self.assertEqual(settings['SLUG_REGEX_SUBSTITUTIONS'],
|
|
[(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)
|