Added support for multiple config files
This commit is contained in:
parent
95c81608c3
commit
8fab13e13a
4 changed files with 92 additions and 10 deletions
2
doc
2
doc
|
@ -1 +1 @@
|
||||||
Subproject commit 4599a339dbd4a2299a8eca575a7b801686d9fc5a
|
Subproject commit d5dc5f794ad1b959b9d4dce47eeb6068e5a75115
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Iterable, Union
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -12,9 +12,9 @@ if __name__ == '__main__':
|
||||||
from matemat.webserver import MatematWebserver
|
from matemat.webserver import MatematWebserver
|
||||||
|
|
||||||
# Use config file name from command line, if present
|
# Use config file name from command line, if present
|
||||||
configfile: str = '/etc/matemat.conf'
|
configfile: Union[str, Iterable[str]] = '/etc/matemat.conf'
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
configfile = sys.argv[1]
|
configfile = sys.argv[1:]
|
||||||
|
|
||||||
# Parse the config file
|
# Parse the config file
|
||||||
config: Dict[str, Any] = parse_config_file(configfile)
|
config: Dict[str, Any] = parse_config_file(configfile)
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Iterable, List, Union
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
def parse_config_file(path: str) -> Dict[str, Any]:
|
def parse_config_file(paths: Union[str, Iterable[str]]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Parse the configuration file at the given path.
|
Parse the configuration file at the given path.
|
||||||
|
|
||||||
:param path: The config file to parse.
|
:param paths: The config file(s) to parse.
|
||||||
:return: A dictionary containing the parsed configuration.
|
:return: A dictionary containing the parsed configuration.
|
||||||
"""
|
"""
|
||||||
# Set up default values
|
# Set up default values
|
||||||
|
@ -30,8 +30,17 @@ def parse_config_file(path: str) -> Dict[str, Any]:
|
||||||
parser: ConfigParser = ConfigParser()
|
parser: ConfigParser = ConfigParser()
|
||||||
# Replace the original option transformation by a string constructor to preserve the case of config keys
|
# Replace the original option transformation by a string constructor to preserve the case of config keys
|
||||||
parser.optionxform = str
|
parser.optionxform = str
|
||||||
# Read the configuration file
|
# Normalize the input argument (turn a scalar into a list and expand ~ in paths)
|
||||||
parser.read(os.path.expanduser(path), 'utf-8')
|
files: List[str] = list()
|
||||||
|
if isinstance(paths, str):
|
||||||
|
files.append(os.path.expanduser(paths))
|
||||||
|
else:
|
||||||
|
for path in paths:
|
||||||
|
if not isinstance(path, str):
|
||||||
|
raise TypeError(f'Not a string: {path}')
|
||||||
|
files.append(os.path.expanduser(path))
|
||||||
|
# Read the configuration files
|
||||||
|
parser.read(files, 'utf-8')
|
||||||
|
|
||||||
# Read values from the [Matemat] section, if present, falling back to default values
|
# Read values from the [Matemat] section, if present, falling back to default values
|
||||||
if 'Matemat' in parser.sections():
|
if 'Matemat' in parser.sections():
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
@ -24,17 +26,54 @@ UploadDir= /var/test/static/upload
|
||||||
DatabaseFile=/var/test/db/test.db
|
DatabaseFile=/var/test/db/test.db
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
_PARTIAL_CONFIG = '''
|
||||||
|
[Matemat]
|
||||||
|
Port=443
|
||||||
|
|
||||||
|
[Pagelets]
|
||||||
|
Name=Matemat (Unit Test 2)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class IterOpenMock:
|
||||||
|
"""
|
||||||
|
Enable mocking of subsequent open() class for different files. Usage:
|
||||||
|
|
||||||
|
with mock.patch('builtins.open', IterOpenMock(['content 1', 'content 2'])):
|
||||||
|
...
|
||||||
|
with open('foo') as f:
|
||||||
|
# Reading from f will yield 'content 1'
|
||||||
|
with open('foo') as f:
|
||||||
|
# Reading from f will yield 'content 2'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, files: List[str]):
|
||||||
|
self.files = files
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return StringIO(self.files[0])
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.files = self.files[1:]
|
||||||
|
|
||||||
|
|
||||||
class TestConfig(TestCase):
|
class TestConfig(TestCase):
|
||||||
|
|
||||||
def test_parse_config_empty_defualt_values(self):
|
def test_parse_config_empty_defualt_values(self):
|
||||||
|
"""
|
||||||
|
Test that default values are set when reading an empty config file.
|
||||||
|
"""
|
||||||
|
# Mock the open() function to return an empty config file example
|
||||||
with patch('builtins.open', return_value=StringIO(_EMPTY_CONFIG)):
|
with patch('builtins.open', return_value=StringIO(_EMPTY_CONFIG)):
|
||||||
|
# The filename is only a placeholder, file content is determined by mocking open
|
||||||
config = parse_config_file('test')
|
config = parse_config_file('test')
|
||||||
|
# Make sure all mandatory values are present
|
||||||
self.assertIn('listen', config)
|
self.assertIn('listen', config)
|
||||||
self.assertIn('port', config)
|
self.assertIn('port', config)
|
||||||
self.assertIn('staticroot', config)
|
self.assertIn('staticroot', config)
|
||||||
self.assertIn('templateroot', config)
|
self.assertIn('templateroot', config)
|
||||||
self.assertIn('pagelet_variables', config)
|
self.assertIn('pagelet_variables', config)
|
||||||
|
# Make sure all mandatory values are set to their default
|
||||||
self.assertEqual('::', config['listen'])
|
self.assertEqual('::', config['listen'])
|
||||||
self.assertEqual(80, config['port'])
|
self.assertEqual(80, config['port'])
|
||||||
self.assertEqual('/var/matemat/static', config['staticroot'])
|
self.assertEqual('/var/matemat/static', config['staticroot'])
|
||||||
|
@ -43,8 +82,14 @@ class TestConfig(TestCase):
|
||||||
self.assertEqual(0, len(config['pagelet_variables']))
|
self.assertEqual(0, len(config['pagelet_variables']))
|
||||||
|
|
||||||
def test_parse_config_full(self):
|
def test_parse_config_full(self):
|
||||||
|
"""
|
||||||
|
Test that all default values are overridden by the values provided in the config file.
|
||||||
|
"""
|
||||||
|
# Mock the open() function to return a full config file example
|
||||||
with patch('builtins.open', return_value=StringIO(_FULL_CONFIG)):
|
with patch('builtins.open', return_value=StringIO(_FULL_CONFIG)):
|
||||||
|
# The filename is only a placeholder, file content is determined by mocking open
|
||||||
config = parse_config_file('test')
|
config = parse_config_file('test')
|
||||||
|
# Make sure all mandatory values are present
|
||||||
self.assertIn('listen', config)
|
self.assertIn('listen', config)
|
||||||
self.assertIn('port', config)
|
self.assertIn('port', config)
|
||||||
self.assertIn('staticroot', config)
|
self.assertIn('staticroot', config)
|
||||||
|
@ -53,7 +98,7 @@ class TestConfig(TestCase):
|
||||||
self.assertIn('Name', config['pagelet_variables'])
|
self.assertIn('Name', config['pagelet_variables'])
|
||||||
self.assertIn('UploadDir', config['pagelet_variables'])
|
self.assertIn('UploadDir', config['pagelet_variables'])
|
||||||
self.assertIn('DatabaseFile', config['pagelet_variables'])
|
self.assertIn('DatabaseFile', config['pagelet_variables'])
|
||||||
|
# Make sure all values are set as described in the config file
|
||||||
self.assertEqual('fe80::0123:45ff:fe67:89ab', config['listen'])
|
self.assertEqual('fe80::0123:45ff:fe67:89ab', config['listen'])
|
||||||
self.assertEqual(8080, config['port'])
|
self.assertEqual(8080, config['port'])
|
||||||
self.assertEqual('/var/test/static', config['staticroot'])
|
self.assertEqual('/var/test/static', config['staticroot'])
|
||||||
|
@ -61,3 +106,31 @@ class TestConfig(TestCase):
|
||||||
self.assertEqual('Matemat\n(Unit Test)', config['pagelet_variables']['Name'])
|
self.assertEqual('Matemat\n(Unit Test)', config['pagelet_variables']['Name'])
|
||||||
self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir'])
|
self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir'])
|
||||||
self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile'])
|
self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile'])
|
||||||
|
|
||||||
|
def test_parse_config_precedence(self):
|
||||||
|
"""
|
||||||
|
Test that config items from files with higher precedence replace items with the same key from files with lower
|
||||||
|
precedence.
|
||||||
|
"""
|
||||||
|
# Mock the open() function to return a full config file on the first call, and a partial config file on the
|
||||||
|
# second call
|
||||||
|
with patch('builtins.open', return_value=IterOpenMock([_FULL_CONFIG, _PARTIAL_CONFIG])):
|
||||||
|
# These filenames are only placeholders, file content is determined by mocking open
|
||||||
|
config = parse_config_file(['full', 'partial'])
|
||||||
|
# Make sure all mandatory values are present
|
||||||
|
self.assertIn('listen', config)
|
||||||
|
self.assertIn('port', config)
|
||||||
|
self.assertIn('staticroot', config)
|
||||||
|
self.assertIn('templateroot', config)
|
||||||
|
self.assertIn('pagelet_variables', config)
|
||||||
|
self.assertIn('Name', config['pagelet_variables'])
|
||||||
|
self.assertIn('UploadDir', config['pagelet_variables'])
|
||||||
|
self.assertIn('DatabaseFile', config['pagelet_variables'])
|
||||||
|
# Make sure all values are set as described in the config files, values from the partial file take precedence
|
||||||
|
self.assertEqual('fe80::0123:45ff:fe67:89ab', config['listen'])
|
||||||
|
self.assertEqual(443, config['port'])
|
||||||
|
self.assertEqual('/var/test/static', config['staticroot'])
|
||||||
|
self.assertEqual('/var/test/templates', config['templateroot'])
|
||||||
|
self.assertEqual('Matemat (Unit Test 2)', config['pagelet_variables']['Name'])
|
||||||
|
self.assertEqual('/var/test/static/upload', config['pagelet_variables']['UploadDir'])
|
||||||
|
self.assertEqual('/var/test/db/test.db', config['pagelet_variables']['DatabaseFile'])
|
||||||
|
|
Loading…
Reference in a new issue