diff options
author | Lars Wirzenius <liw@liw.fi> | 2015-12-23 22:06:57 +0100 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2015-12-23 22:06:57 +0100 |
commit | 831f947c1e1466c8ff1f27ae6c424c3e6e4e0bb7 (patch) | |
tree | 775434004c3cfa460cbf67f73b560edd8cf9a098 | |
parent | 6a1b0a585aea1c5d76384e88f50f8cebd030bf3a (diff) | |
download | cliapp-831f947c1e1466c8ff1f27ae6c424c3e6e4e0bb7.tar.gz |
Add support for YAML config files
INI files continue to be supported, too.
-rw-r--r-- | cliapp/settings.py | 76 | ||||
-rw-r--r-- | cliapp/settings_tests.py | 158 |
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): |