Skip to content

Commit

Permalink
Rework Config-File Handling (#3244)
Browse files Browse the repository at this point in the history
  • Loading branch information
manuth authored and Kwpolska committed Mar 10, 2019
1 parent f3e8249 commit d939b88
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 14 deletions.
1 change: 1 addition & 0 deletions AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
* `Leandro Poblet <https://github.com/DoctorMalboro>`_
* `Luis Miguel Morillas <https://github.com/lmorillas>`_
* `Manuel Kaufmann <https://github.com/humitos>`_
* `Manuel Thalmann <https://github.com/manuth>`_
* `Marcelo MD <https://github.com/marcelomd>`_
* `Marcos Dione <https://github.com/StyXman>`_
* `Mariano Guerra <https://github.com/marianoguerra>`_
Expand Down
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------
Expand Down
36 changes: 31 additions & 5 deletions docs/manual.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 = "[email protected]"
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
---------------------
Expand Down
1 change: 0 additions & 1 deletion dodo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

import os
import fnmatch
import subprocess

DOIT_CONFIG = {
'default_tasks': ['flake8', 'test'],
Expand Down
27 changes: 19 additions & 8 deletions nikola/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

"""The main function of Nikola."""

import imp
import os
import shutil
import sys
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -117,23 +115,36 @@ 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))
return 1
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))
Expand Down
30 changes: 30 additions & 0 deletions tests/data/test_config/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
import time

BLOG_AUTHOR = "Your Name"
BLOG_TITLE = "Demo Site"
SITE_URL = "https://example.com/"
BLOG_EMAIL = "[email protected]"
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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import conf

globals().update(vars(conf))
ADDITIONAL_METADATA = {
"ID": "illegal"
}
6 changes: 6 additions & 0 deletions tests/data/test_config/prod.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import conf

globals().update(vars(conf))
ADDITIONAL_METADATA = {
"ID": "prod"
}
44 changes: 44 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit d939b88

Please sign in to comment.