Skip to content

Commit d939b88

Browse files
neurolagKwpolska
authored andcommitted
Rework Config-File Handling (getnikola#3244)
1 parent f3e8249 commit d939b88

File tree

9 files changed

+138
-14
lines changed

9 files changed

+138
-14
lines changed

AUTHORS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
* `Leandro Poblet <https://github.com/DoctorMalboro>`_
6565
* `Luis Miguel Morillas <https://github.com/lmorillas>`_
6666
* `Manuel Kaufmann <https://github.com/humitos>`_
67+
* `Manuel Thalmann <https://github.com/manuth>`_
6768
* `Marcelo MD <https://github.com/marcelomd>`_
6869
* `Marcos Dione <https://github.com/StyXman>`_
6970
* `Mariano Guerra <https://github.com/marianoguerra>`_

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Features
1313
* Add Interlingua translation by Alberto Mardegan
1414
* Add Afrikaans translation by Friedel Wolff
1515
* Support for docutils.conf (Issue #3188)
16+
* Add support for inherited config-files
1617

1718
Bugfixes
1819
--------

docs/manual.rst

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,7 +1487,10 @@ you can't, this will work.
14871487
Configuration
14881488
-------------
14891489

1490-
The configuration file is called ``conf.py`` and can be used to customize a lot of
1490+
You can pass a configuration file to ``nikola`` by using the ``--conf`` command line switch.
1491+
Otherwise the ``conf.py`` file in the root of the Nikola website will be used.
1492+
1493+
The configuration file can be used to customize a lot of
14911494
what Nikola does. Its syntax is python, but if you don't know the language, it
14921495
still should not be terribly hard to grasp.
14931496

@@ -1511,10 +1514,33 @@ them. For those options, two types of values can be provided:
15111514
* a string, which will be used for all languages
15121515
* a dict of language-value pairs, to have different values in each language
15131516

1514-
.. note:: It is possible to load the configuration from another file by specifying
1515-
``--conf=path/to/other.file`` on Nikola's command line. For example, to
1516-
build your blog using the configuration file ``configurations/test.conf.py``,
1517-
you have to execute ``nikola build --conf=configurations/test.conf.py``.
1517+
.. note::
1518+
As of version 8.0.3 it is possible to create configuration files which inherit values from other Python files.
1519+
This might be useful if you're working with similar environments.
1520+
1521+
Example:
1522+
conf.py:
1523+
.. code:: python
1524+
1525+
BLOG_AUTHOR = "Your Name"
1526+
BLOG_TITLE = "Demo Site"
1527+
SITE_URL = "https://yourname.github.io/demo-site
1528+
BLOG_EMAIL = "[email protected]"
1529+
BLOG_DESCRIPTION = "This is a demo site for Nikola."
1530+
1531+
debug.conf.py:
1532+
.. code:: python
1533+
1534+
import conf
1535+
globals().update(vars(conf))
1536+
SITE_URL = "http://localhost:8000/"
1537+
1538+
or
1539+
1540+
.. code:: python
1541+
1542+
from conf import *
1543+
SITE_URL = "http://localhost:8000/"
15181544
15191545
Customizing Your Site
15201546
---------------------

dodo.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
import os
33
import fnmatch
4-
import subprocess
54

65
DOIT_CONFIG = {
76
'default_tasks': ['flake8', 'test'],

nikola/__main__.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
"""The main function of Nikola."""
2828

29+
import imp
2930
import os
3031
import shutil
3132
import sys
@@ -56,9 +57,6 @@
5657
except ImportError:
5758
pass # This is only so raw_input/input does nicer things if it's available
5859

59-
60-
import importlib.machinery
61-
6260
config = {}
6361

6462
# DO NOT USE unless you know what you are doing!
@@ -117,23 +115,36 @@ def main(args=None):
117115
os.chdir(root)
118116
# Help and imports don't require config, but can use one if it exists
119117
needs_config_file = (argname != 'help') and not argname.startswith('import_')
118+
if needs_config_file:
119+
if root is None:
120+
LOGGER.error("The command could not be executed: You're not in a nikola website.")
121+
return 1
122+
else:
123+
LOGGER.info("Website root: '{0}'".format(root))
120124
else:
121125
needs_config_file = False
122126

123-
sys.path.append('')
127+
old_path = sys.path
128+
old_modules = sys.modules
129+
124130
try:
125-
loader = importlib.machinery.SourceFileLoader("conf", conf_filename)
126-
conf = loader.load_module()
127-
config = conf.__dict__
131+
sys.path = sys.path[:]
132+
sys.modules = sys.modules.copy()
133+
sys.path.insert(0, os.path.dirname(conf_filename))
134+
with open(conf_filename, "rb") as file:
135+
config = imp.load_module(conf_filename, file, conf_filename, (None, "rb", imp.PY_SOURCE)).__dict__
128136
except Exception:
137+
config = {}
129138
if os.path.exists(conf_filename):
130139
msg = traceback.format_exc(0)
131140
LOGGER.error('"{0}" cannot be parsed.\n{1}'.format(conf_filename, msg))
132141
return 1
133142
elif needs_config_file and conf_filename_changed:
134143
LOGGER.error('Cannot find configuration file "{0}".'.format(conf_filename))
135144
return 1
136-
config = {}
145+
finally:
146+
sys.path = old_path
147+
sys.modules = old_modules
137148

138149
if conf_filename_changed:
139150
LOGGER.info("Using config file '{0}'".format(conf_filename))

tests/data/test_config/conf.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# -*- coding: utf-8 -*-
2+
import time
3+
4+
BLOG_AUTHOR = "Your Name"
5+
BLOG_TITLE = "Demo Site"
6+
SITE_URL = "https://example.com/"
7+
BLOG_EMAIL = "[email protected]"
8+
BLOG_DESCRIPTION = "This is a demo site for Nikola."
9+
DEFAULT_LANG = "en"
10+
CATEGORY_ALLOW_HIERARCHIES = False
11+
CATEGORY_OUTPUT_FLAT_HIERARCHY = False
12+
HIDDEN_CATEGORIES = []
13+
HIDDEN_AUTHORS = ['Guest']
14+
LICENSE = ""
15+
16+
CONTENT_FOOTER_FORMATS = {
17+
DEFAULT_LANG: (
18+
(),
19+
{
20+
"email": BLOG_EMAIL,
21+
"author": BLOG_AUTHOR,
22+
"date": time.gmtime().tm_year,
23+
"license": LICENSE
24+
}
25+
)
26+
}
27+
28+
ADDITIONAL_METADATA = {
29+
"ID": "conf"
30+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import conf
2+
3+
globals().update(vars(conf))
4+
ADDITIONAL_METADATA = {
5+
"ID": "illegal"
6+
}

tests/data/test_config/prod.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import conf
2+
3+
globals().update(vars(conf))
4+
ADDITIONAL_METADATA = {
5+
"ID": "prod"
6+
}

tests/test_config.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# -*- coding: utf-8 -*-
2+
import os
3+
import re
4+
5+
from nikola import __main__ as nikola
6+
7+
from .base import BaseTestCase
8+
9+
10+
class ConfigTest(BaseTestCase):
11+
"""Provides tests for the configuration-file handling."""
12+
@classmethod
13+
def setUpClass(self):
14+
self.metadata_option = "ADDITIONAL_METADATA"
15+
script_root = os.path.dirname(__file__)
16+
test_dir = os.path.join(script_root, "data", "test_config")
17+
nikola.main(["--conf=" + os.path.join(test_dir, "conf.py")])
18+
self.simple_config = nikola.config
19+
nikola.main(["--conf=" + os.path.join(test_dir, "prod.py")])
20+
self.complex_config = nikola.config
21+
nikola.main(["--conf=" + os.path.join(test_dir, "config.with+illegal(module)name.characters.py")])
22+
self.complex_filename_config = nikola.config
23+
self.check_base_equality(self.complex_filename_config)
24+
25+
@classmethod
26+
def check_base_equality(self, config):
27+
"""Check whether the specified `config` equals the base config."""
28+
for option in self.simple_config.keys():
29+
if re.match("^[A-Z]+(_[A-Z]+)*$", option) and option != self.metadata_option:
30+
assert self.simple_config[option] == self.complex_config[option]
31+
32+
def test_simple_config(self):
33+
"""Check whether configuration-files without ineritance are interpreted correctly."""
34+
assert self.simple_config[self.metadata_option]["ID"] == "conf"
35+
36+
def test_inherited_config(self):
37+
"""Check whether configuration-files with ineritance are interpreted correctly."""
38+
self.check_base_equality(config=self.complex_config)
39+
assert self.complex_config[self.metadata_option]["ID"] == "prod"
40+
41+
def test_config_with_illegal_filename(self):
42+
"""Check whether files with illegal module-name characters can be set as config-files, too."""
43+
self.check_base_equality(config=self.complex_filename_config)
44+
assert self.complex_filename_config[self.metadata_option]["ID"] == "illegal"

0 commit comments

Comments
 (0)