From d939b8864af4e55bde42eeeae93612555fc82eae Mon Sep 17 00:00:00 2001 From: Manuel Thalmann Date: Sun, 10 Mar 2019 18:50:58 +0100 Subject: [PATCH] Rework Config-File Handling (#3244) --- AUTHORS.txt | 1 + CHANGES.txt | 1 + docs/manual.rst | 36 ++++++++++++--- dodo.py | 1 - nikola/__main__.py | 27 ++++++++---- tests/data/test_config/conf.py | 30 +++++++++++++ ...fig.with+illegal(module)name.characters.py | 6 +++ tests/data/test_config/prod.py | 6 +++ tests/test_config.py | 44 +++++++++++++++++++ 9 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 tests/data/test_config/conf.py create mode 100644 tests/data/test_config/config.with+illegal(module)name.characters.py create mode 100644 tests/data/test_config/prod.py create mode 100644 tests/test_config.py diff --git a/AUTHORS.txt b/AUTHORS.txt index f0966a78a1..c53bee8c56 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -64,6 +64,7 @@ * `Leandro Poblet `_ * `Luis Miguel Morillas `_ * `Manuel Kaufmann `_ +* `Manuel Thalmann `_ * `Marcelo MD `_ * `Marcos Dione `_ * `Mariano Guerra `_ diff --git a/CHANGES.txt b/CHANGES.txt index 96a16adc75..3e4794f57a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -13,6 +13,7 @@ Features * Add Interlingua translation by Alberto Mardegan * Add Afrikaans translation by Friedel Wolff * Support for docutils.conf (Issue #3188) +* Add support for inherited config-files Bugfixes -------- diff --git a/docs/manual.rst b/docs/manual.rst index a8cb3b5dfa..ccb8324802 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -1487,7 +1487,10 @@ you can't, this will work. Configuration ------------- -The configuration file is called ``conf.py`` and can be used to customize a lot of +You can pass a configuration file to ``nikola`` by using the ``--conf`` command line switch. +Otherwise the ``conf.py`` file in the root of the Nikola website will be used. + +The configuration file can be used to customize a lot of what Nikola does. Its syntax is python, but if you don't know the language, it still should not be terribly hard to grasp. @@ -1511,10 +1514,33 @@ them. For those options, two types of values can be provided: * a string, which will be used for all languages * a dict of language-value pairs, to have different values in each language -.. note:: It is possible to load the configuration from another file by specifying - ``--conf=path/to/other.file`` on Nikola's command line. For example, to - build your blog using the configuration file ``configurations/test.conf.py``, - you have to execute ``nikola build --conf=configurations/test.conf.py``. +.. note:: + As of version 8.0.3 it is possible to create configuration files which inherit values from other Python files. + This might be useful if you're working with similar environments. + + Example: + conf.py: + .. code:: python + + BLOG_AUTHOR = "Your Name" + BLOG_TITLE = "Demo Site" + SITE_URL = "https://yourname.github.io/demo-site + BLOG_EMAIL = "joe@demo.site" + BLOG_DESCRIPTION = "This is a demo site for Nikola." + + debug.conf.py: + .. code:: python + + import conf + globals().update(vars(conf)) + SITE_URL = "http://localhost:8000/" + + or + + .. code:: python + + from conf import * + SITE_URL = "http://localhost:8000/" Customizing Your Site --------------------- diff --git a/dodo.py b/dodo.py index 4c0d34992f..1d78634989 100644 --- a/dodo.py +++ b/dodo.py @@ -1,7 +1,6 @@ import os import fnmatch -import subprocess DOIT_CONFIG = { 'default_tasks': ['flake8', 'test'], diff --git a/nikola/__main__.py b/nikola/__main__.py index 09235cd401..a7764fcce6 100644 --- a/nikola/__main__.py +++ b/nikola/__main__.py @@ -26,6 +26,7 @@ """The main function of Nikola.""" +import imp import os import shutil import sys @@ -56,9 +57,6 @@ except ImportError: pass # This is only so raw_input/input does nicer things if it's available - -import importlib.machinery - config = {} # DO NOT USE unless you know what you are doing! @@ -117,15 +115,26 @@ def main(args=None): os.chdir(root) # Help and imports don't require config, but can use one if it exists needs_config_file = (argname != 'help') and not argname.startswith('import_') + if needs_config_file: + if root is None: + LOGGER.error("The command could not be executed: You're not in a nikola website.") + return 1 + else: + LOGGER.info("Website root: '{0}'".format(root)) else: needs_config_file = False - sys.path.append('') + old_path = sys.path + old_modules = sys.modules + try: - loader = importlib.machinery.SourceFileLoader("conf", conf_filename) - conf = loader.load_module() - config = conf.__dict__ + sys.path = sys.path[:] + sys.modules = sys.modules.copy() + sys.path.insert(0, os.path.dirname(conf_filename)) + with open(conf_filename, "rb") as file: + config = imp.load_module(conf_filename, file, conf_filename, (None, "rb", imp.PY_SOURCE)).__dict__ except Exception: + config = {} if os.path.exists(conf_filename): msg = traceback.format_exc(0) LOGGER.error('"{0}" cannot be parsed.\n{1}'.format(conf_filename, msg)) @@ -133,7 +142,9 @@ def main(args=None): elif needs_config_file and conf_filename_changed: LOGGER.error('Cannot find configuration file "{0}".'.format(conf_filename)) return 1 - config = {} + finally: + sys.path = old_path + sys.modules = old_modules if conf_filename_changed: LOGGER.info("Using config file '{0}'".format(conf_filename)) diff --git a/tests/data/test_config/conf.py b/tests/data/test_config/conf.py new file mode 100644 index 0000000000..2a2d1b8e5d --- /dev/null +++ b/tests/data/test_config/conf.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +import time + +BLOG_AUTHOR = "Your Name" +BLOG_TITLE = "Demo Site" +SITE_URL = "https://example.com/" +BLOG_EMAIL = "joe@demo.site" +BLOG_DESCRIPTION = "This is a demo site for Nikola." +DEFAULT_LANG = "en" +CATEGORY_ALLOW_HIERARCHIES = False +CATEGORY_OUTPUT_FLAT_HIERARCHY = False +HIDDEN_CATEGORIES = [] +HIDDEN_AUTHORS = ['Guest'] +LICENSE = "" + +CONTENT_FOOTER_FORMATS = { + DEFAULT_LANG: ( + (), + { + "email": BLOG_EMAIL, + "author": BLOG_AUTHOR, + "date": time.gmtime().tm_year, + "license": LICENSE + } + ) +} + +ADDITIONAL_METADATA = { + "ID": "conf" +} diff --git a/tests/data/test_config/config.with+illegal(module)name.characters.py b/tests/data/test_config/config.with+illegal(module)name.characters.py new file mode 100644 index 0000000000..39a8aeb7ce --- /dev/null +++ b/tests/data/test_config/config.with+illegal(module)name.characters.py @@ -0,0 +1,6 @@ +import conf + +globals().update(vars(conf)) +ADDITIONAL_METADATA = { + "ID": "illegal" +} diff --git a/tests/data/test_config/prod.py b/tests/data/test_config/prod.py new file mode 100644 index 0000000000..38388275cd --- /dev/null +++ b/tests/data/test_config/prod.py @@ -0,0 +1,6 @@ +import conf + +globals().update(vars(conf)) +ADDITIONAL_METADATA = { + "ID": "prod" +} diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000000..9cb776a022 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import os +import re + +from nikola import __main__ as nikola + +from .base import BaseTestCase + + +class ConfigTest(BaseTestCase): + """Provides tests for the configuration-file handling.""" + @classmethod + def setUpClass(self): + self.metadata_option = "ADDITIONAL_METADATA" + script_root = os.path.dirname(__file__) + test_dir = os.path.join(script_root, "data", "test_config") + nikola.main(["--conf=" + os.path.join(test_dir, "conf.py")]) + self.simple_config = nikola.config + nikola.main(["--conf=" + os.path.join(test_dir, "prod.py")]) + self.complex_config = nikola.config + nikola.main(["--conf=" + os.path.join(test_dir, "config.with+illegal(module)name.characters.py")]) + self.complex_filename_config = nikola.config + self.check_base_equality(self.complex_filename_config) + + @classmethod + def check_base_equality(self, config): + """Check whether the specified `config` equals the base config.""" + for option in self.simple_config.keys(): + if re.match("^[A-Z]+(_[A-Z]+)*$", option) and option != self.metadata_option: + assert self.simple_config[option] == self.complex_config[option] + + def test_simple_config(self): + """Check whether configuration-files without ineritance are interpreted correctly.""" + assert self.simple_config[self.metadata_option]["ID"] == "conf" + + def test_inherited_config(self): + """Check whether configuration-files with ineritance are interpreted correctly.""" + self.check_base_equality(config=self.complex_config) + assert self.complex_config[self.metadata_option]["ID"] == "prod" + + def test_config_with_illegal_filename(self): + """Check whether files with illegal module-name characters can be set as config-files, too.""" + self.check_base_equality(config=self.complex_filename_config) + assert self.complex_filename_config[self.metadata_option]["ID"] == "illegal"