summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2015-12-23 22:06:57 +0100
committerLars Wirzenius <liw@liw.fi>2015-12-23 22:06:57 +0100
commit831f947c1e1466c8ff1f27ae6c424c3e6e4e0bb7 (patch)
tree775434004c3cfa460cbf67f73b560edd8cf9a098
parent6a1b0a585aea1c5d76384e88f50f8cebd030bf3a (diff)
downloadcliapp-831f947c1e1466c8ff1f27ae6c424c3e6e4e0bb7.tar.gz
Add support for YAML config files
INI files continue to be supported, too.
-rw-r--r--cliapp/settings.py76
-rw-r--r--cliapp/settings_tests.py158
2 files changed, 206 insertions, 28 deletions
diff --git a/cliapp/settings.py b/cliapp/settings.py
index bfc6b4a..1a88a6b 100644
--- a/cliapp/settings.py
+++ b/cliapp/settings.py
@@ -21,6 +21,8 @@ import os
import re
import sys
+import yaml
+
import cliapp
from cliapp.genman import ManpageGenerator
@@ -115,7 +117,10 @@ class StringListSetting(Setting):
return self._strings
def set_value(self, strings):
- self._strings = strings
+ if type(strings) != list:
+ self._strings = [strings]
+ else:
+ self._strings = strings
self.using_default_value = False
def has_value(self):
@@ -285,6 +290,7 @@ class Settings(object):
def __init__(self, progname, version, usage=None, description=None,
epilog=None):
self._settingses = dict()
+ self._all_config_data = {}
self._canonical_names = list()
self.version = version
@@ -717,8 +723,10 @@ class Settings(object):
configs = []
configs.append('/etc/%s.conf' % self.progname)
+ configs.append('/etc/%s.yaml' % self.progname)
configs += self.listconfs('/etc/%s' % self.progname)
configs.append(os.path.expanduser('~/.%s.conf' % self.progname))
+ configs.append(os.path.expanduser('~/.%s.yaml' % self.progname))
configs += self.listconfs(
os.path.expanduser('~/.config/%s' % self.progname))
@@ -727,10 +735,11 @@ class Settings(object):
def listconfs(self, dirname, listdir=os.listdir):
'''Return list of pathnames to config files in dirname.
- Config files are expectd to have names ending in '.conf'.
+ Config files are expected to have names ending in '.conf' or
+ '.yaml'.
- If dirname does not exist or is not a directory,
- return empty list.
+ If dirname does not exist or is not a directory, return empty
+ list.
'''
@@ -741,7 +750,7 @@ class Settings(object):
basenames.sort(key=lambda s: [ord(c) for c in s])
return [os.path.join(dirname, x)
for x in basenames
- if x.endswith('.conf')]
+ if x.endswith('.conf') or x.endswith('.yaml')]
def _get_config_files(self):
if self._config_files is None:
@@ -768,27 +777,54 @@ class Settings(object):
'''
- cp = ConfigParser.ConfigParser()
- cp.add_section('config')
+ self._all_config_data = {}
for pathname in self.config_files:
try:
f = open_file(pathname)
+ if pathname.endswith('.yaml'):
+ self._read_yaml(pathname, f)
+ else:
+ self._read_ini(pathname, f)
+ f.close()
except IOError: # pragma: no cover
if pathname in self._required_config_files:
raise
- else:
- cp.readfp(f)
- f.close()
- for name in cp.options('config'):
- value = cp.get('config', name)
- s = self.set_from_raw_string(pathname, name, value)
- if hasattr(s, 'using_default_value'):
- s.using_default_value = True
+ def _read_ini(self, pathname, f):
+ cp = ConfigParser.ConfigParser()
+ cp.add_section('config')
+ cp.readfp(f)
+ for name in cp.options('config'):
+ value = cp.get('config', name)
+ s = self.set_from_raw_string(pathname, name, value)
+ if hasattr(s, 'using_default_value'):
+ s.using_default_value = True
+
+ for section in [s for s in cp.sections() if s != 'config']:
+ if section not in self._all_config_data:
+ self._all_config_data[section] = {}
+ section_data = self._all_config_data[section]
+ for option in cp.options(section):
+ section_data[option] = cp.get(section, option)
+
+ def _read_yaml(self, pathname, f):
+ obj = yaml.safe_load(f)
+ config = obj.get('config', {})
+ for name, value in config.items():
+ if name not in self._settingses:
+ raise UnknownConfigVariable(pathname, name)
+ s = self._settingses[name]
+ s.set_value(value)
+ if hasattr(s, 'using_default_value'):
+ s.using_default_value = True
- # Remember the ConfigParser for use in as_cp later on.
- self._cp = cp
+ for section in [s for s in obj if s != 'config']:
+ if section not in self._all_config_data:
+ self._all_config_data[section] = {}
+ section_data = self._all_config_data[section]
+ for option in obj[section]:
+ section_data[option] = obj[section][option]
def _generate_manpage(self, o, dummy, value, p): # pragma: no cover
template = open(value).read()
@@ -805,16 +841,16 @@ class Settings(object):
meanings it desires to the section names.
'''
+
cp = ConfigParser.ConfigParser()
cp.add_section('config')
for name in self._canonical_names:
cp.set('config', name, self._settingses[name].format())
- for section in self._cp.sections():
+ for section in self._all_config_data:
if section != 'config':
cp.add_section(section)
- for option in self._cp.options(section):
- value = self._cp.get(section, option)
+ for option, value in self._all_config_data[section].items():
cp.set(section, option, value)
return cp
diff --git a/cliapp/settings_tests.py b/cliapp/settings_tests.py
index 57fed59..efb7db3 100644
--- a/cliapp/settings_tests.py
+++ b/cliapp/settings_tests.py
@@ -165,17 +165,35 @@ class SettingsTests(unittest.TestCase):
self.settings['foo'] = ''
self.assertFalse(self.settings['foo'])
- def test_sets_boolean_to_true_from_config_file(self):
+ def test_sets_boolean_to_true_from_ini_file(self):
def fake_open(filename):
return StringIO.StringIO('[config]\nfoo = yes\n')
self.settings.boolean(['foo'], 'foo help')
+ self.settings.config_files = ['foo.conf']
self.settings.load_configs(open_file=fake_open)
self.assertEqual(self.settings['foo'], True)
- def test_sets_boolean_to_false_from_config_file(self):
+ def test_sets_boolean_to_false_from_ini_file(self):
def fake_open(filename):
return StringIO.StringIO('[config]\nfoo = False\n')
self.settings.boolean(['foo'], 'foo help')
+ self.settings.config_files = ['foo.conf']
+ self.settings.load_configs(open_file=fake_open)
+ self.assertEqual(self.settings['foo'], False)
+
+ def test_sets_boolean_to_true_from_yaml_file(self):
+ def fake_open(filename):
+ return StringIO.StringIO('config:\n foo: true\n')
+ self.settings.boolean(['foo'], 'foo help')
+ self.settings.config_files = ['foo.yaml']
+ self.settings.load_configs(open_file=fake_open)
+ self.assertEqual(self.settings['foo'], True)
+
+ def test_sets_boolean_to_false_from_yaml_file(self):
+ def fake_open(filename):
+ return StringIO.StringIO('config:\n foo: false\n')
+ self.settings.boolean(['foo'], 'foo help')
+ self.settings.config_files = ['foo.yaml']
self.settings.load_configs(open_file=fake_open)
self.assertEqual(self.settings['foo'], False)
@@ -258,7 +276,7 @@ class SettingsTests(unittest.TestCase):
self.assertEqual(self.settings.config_files,
self.settings.default_config_files + ['./foo'])
- def test_loads_config_files(self):
+ def test_loads_ini_files(self):
def mock_open(filename, mode=None):
return StringIO.StringIO('''\
@@ -271,7 +289,20 @@ foo = yeehaa
self.settings.load_configs(open_file=mock_open)
self.assertEqual(self.settings['foo'], 'yeehaa')
- def test_loads_string_list_from_config_files(self):
+ def test_loads_yaml_files(self):
+
+ def mock_open(filename, mode=None):
+ return StringIO.StringIO('''\
+config:
+ foo: yeehaa
+''')
+
+ self.settings.string(['foo'], 'foo help')
+ self.settings.config_files = ['whatever.yaml']
+ self.settings.load_configs(open_file=mock_open)
+ self.assertEqual(self.settings['foo'], 'yeehaa')
+
+ def test_loads_string_list_from_ini_files(self):
def mock_open(filename, mode=None):
return StringIO.StringIO('''\
@@ -290,7 +321,26 @@ comma = ping, pong, "foo,bar"
self.assertEqual(self.settings['bar'], ['ping', 'pong'])
self.assertEqual(self.settings['comma'], ['ping', 'pong', 'foo,bar'])
- def test_handles_defaults_with_config_files(self):
+ def test_loads_string_list_from_yaml_files(self):
+
+ def mock_open(filename, mode=None):
+ return StringIO.StringIO('''\
+config:
+ foo: yeehaa
+ bar: [ping, pong]
+ comma: [ping, pong, "foo,bar"]
+''')
+
+ self.settings.string_list(['foo'], 'foo help')
+ self.settings.string_list(['bar'], 'bar help')
+ self.settings.string_list(['comma'], 'comma help')
+ self.settings.config_files = ['whatever.yaml']
+ self.settings.load_configs(open_file=mock_open)
+ self.assertEqual(self.settings['foo'], ['yeehaa'])
+ self.assertEqual(self.settings['bar'], ['ping', 'pong'])
+ self.assertEqual(self.settings['comma'], ['ping', 'pong', 'foo,bar'])
+
+ def test_handles_defaults_with_ini_files(self):
def mock_open(filename, mode=None):
return StringIO.StringIO('''\
@@ -304,7 +354,21 @@ comma = ping, pong, "foo,bar"
self.assertEqual(self.settings['foo'], 'foo')
self.assertEqual(self.settings['bar'], ['bar'])
- def test_handles_overridden_defaults_with_config_files(self):
+ def test_handles_defaults_with_yaml_files(self):
+
+ def mock_open(filename, mode=None):
+ return StringIO.StringIO('''\
+config: {}
+''')
+
+ self.settings.string(['foo'], 'foo help', default='foo')
+ self.settings.string_list(['bar'], 'bar help', default=['bar'])
+ self.settings.config_files = ['whatever.yaml']
+ self.settings.load_configs(open_file=mock_open)
+ self.assertEqual(self.settings['foo'], 'foo')
+ self.assertEqual(self.settings['bar'], ['bar'])
+
+ def test_handles_overridden_defaults_with_ini_files(self):
def mock_open(filename, mode=None):
return StringIO.StringIO('''\
@@ -320,7 +384,23 @@ bar = ping, pong
self.assertEqual(self.settings['foo'], 'yeehaa')
self.assertEqual(self.settings['bar'], ['ping', 'pong'])
- def test_handles_values_from_config_files_overridden_on_command_line(self):
+ def test_handles_overridden_defaults_with_yaml_files(self):
+
+ def mock_open(filename, mode=None):
+ return StringIO.StringIO('''\
+config:
+ foo: yeehaa
+ bar: [ping, pong]
+''')
+
+ self.settings.string(['foo'], 'foo help', default='foo')
+ self.settings.string_list(['bar'], 'bar help', default=['bar'])
+ self.settings.config_files = ['whatever.yaml']
+ self.settings.load_configs(open_file=mock_open)
+ self.assertEqual(self.settings['foo'], 'yeehaa')
+ self.assertEqual(self.settings['bar'], ['ping', 'pong'])
+
+ def test_handles_values_from_ini_files_overridden_on_command_line(self):
def mock_open(filename, mode=None):
return StringIO.StringIO('''\
@@ -338,7 +418,25 @@ bar = ping, pong
self.assertEqual(self.settings['foo'], 'red')
self.assertEqual(self.settings['bar'], ['blue', 'white,comma'])
- def test_load_configs_raises_error_for_unknown_variable(self):
+ def test_handles_values_from_yaml_files_overridden_on_command_line(self):
+
+ def mock_open(filename, mode=None):
+ return StringIO.StringIO('''\
+config:
+ foo: yeehaa
+ bar: [ping, pong]
+''')
+
+ self.settings.string(['foo'], 'foo help', default='foo')
+ self.settings.string_list(['bar'], 'bar help', default=['bar'])
+ self.settings.config_files = ['whatever.yaml']
+ self.settings.load_configs(open_file=mock_open)
+ self.settings.parse_args(
+ ['--foo=red', '--bar=blue', '--bar=white,comma'])
+ self.assertEqual(self.settings['foo'], 'red')
+ self.assertEqual(self.settings['bar'], ['blue', 'white,comma'])
+
+ def test_load_configs_raises_error_for_unknown_variable_in_ini(self):
def mock_open(filename, mode=None):
return StringIO.StringIO('''\
@@ -351,6 +449,50 @@ unknown = variable
self.settings.load_configs,
open_file=mock_open)
+ def test_load_configs_raises_error_for_unknown_variable_in_yaml(self):
+
+ def mock_open(filename, mode=None):
+ return StringIO.StringIO('''\
+config:
+ unknown: yeehaa
+''')
+
+ self.settings.string_list(['foo'], 'foo help')
+ self.settings.config_files = ['whatever.yaml']
+ self.assertRaises(
+ cliapp.UnknownConfigVariable,
+ self.settings.load_configs, open_file=mock_open)
+
+ def test_load_configs_remembers_extra_sections_in_ini(self):
+
+ def mock_open(filename, mode=None):
+ return StringIO.StringIO('''\
+[extra]
+something = else
+''')
+
+ self.settings.string_list(['foo'], 'foo help')
+ self.settings.config_files = ['whatever.conf']
+ self.settings.load_configs(open_file=mock_open)
+ cp = self.settings.as_cp()
+ self.assertEqual(cp.sections(), ['config', 'extra'])
+ self.assertEqual(cp.get('extra', 'something'), 'else')
+
+ def test_load_configs_remembers_extra_sections_in_yaml(self):
+
+ def mock_open(filename, mode=None):
+ return StringIO.StringIO('''\
+extra:
+ something: else
+''')
+
+ self.settings.string_list(['foo'], 'foo help')
+ self.settings.config_files = ['whatever.yaml']
+ self.settings.load_configs(open_file=mock_open)
+ cp = self.settings.as_cp()
+ self.assertEqual(cp.sections(), ['config', 'extra'])
+ self.assertEqual(cp.get('extra', 'something'), 'else')
+
def test_load_configs_ignore_errors_opening_a_file(self):
def mock_open(filename, mode=None):