| Index: chainedconfigparser.py | 
| =================================================================== | 
| --- a/chainedconfigparser.py | 
| +++ b/chainedconfigparser.py | 
| @@ -4,7 +4,8 @@ | 
| # License, v. 2.0. If a copy of the MPL was not distributed with this | 
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | 
| -import os, codecs, ConfigParser | 
| +import os | 
| +import ConfigParser | 
| class Item(tuple): | 
| def __new__(cls, name, value, source): | 
| @@ -12,8 +13,16 @@ | 
| result.source = source | 
| return result | 
| -class ChainedConfigParser: | 
| - """ | 
| +class DiffForUnknownOptionError(ConfigParser.Error): | 
| + def __init__(self, option, section): | 
| + ConfigParser.Error.__init__(self, 'Failed to apply diff for unknown option ' | 
| + '%r in section %r' % (option, section)) | 
| + self.option = option | 
| + self.section = section | 
| + self.args = (option, section) | 
| + | 
| +class ChainedConfigParser(ConfigParser.SafeConfigParser): | 
| + ''' | 
| This class provides essentially the same interfaces as SafeConfigParser but | 
| allows chaining configuration files so that one config file provides the | 
| default values for the other. To specify the config file to inherit from | 
| @@ -22,6 +31,13 @@ | 
| [default] | 
| inherit = foo/bar.config | 
| + It is also possible to add values to or remove values from | 
| + whitespace-separated lists given by an inherited option: | 
| + | 
| + [section] | 
| + opt1 += foo | 
| + opt2 -= bar | 
| + | 
| The value of the inherit option has to be a relative path with forward | 
| slashes as delimiters. Up to 5 configuration files can be chained this way, | 
| longer chains are disallowed to deal with circular references. | 
| @@ -33,82 +49,103 @@ | 
| method is provided to get the path of the configuration file defining this | 
| option (for relative paths). Items returned by the items() function also | 
| have a source attribute serving the same purpose. | 
| - """ | 
| + ''' | 
| - def __init__(self, path): | 
| - self.chain = [] | 
| - self.read_path(path) | 
| + def __init__(self): | 
| + ConfigParser.SafeConfigParser.__init__(self) | 
| + self._origin = {} | 
| - def read_path(self, path): | 
| - if len(self.chain) >= 5: | 
| - raise Exception('Too much inheritance in config files') | 
| + def _get_parser_chain(self, parser, filename): | 
| + parsers = [] | 
| - config = ConfigParser.SafeConfigParser() | 
| - config.optionxform = str | 
| - config.source_path = path | 
| - handle = codecs.open(path, 'rb', encoding='utf-8') | 
| - config.readfp(handle) | 
| - handle.close() | 
| - self.chain.append(config) | 
| + while True: | 
| + parsers.insert(0, (parser, filename)) | 
| - if config.has_section('default') and config.has_option('default', 'inherit'): | 
| - parts = config.get('default', 'inherit').split('/') | 
| - defaults_path = os.path.join(os.path.dirname(path), *parts) | 
| - self.read_path(defaults_path) | 
| + try: | 
| + inherit = parser.get('default', 'inherit') | 
| + except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): | 
| + return parsers | 
| - def defaults(self): | 
| - result = {} | 
| - for config in reverse(self.chain): | 
| - for key, value in config.defaults().iteritems(): | 
| - result[key] = value | 
| - return result | 
| + filename = os.path.join(os.path.dirname(filename), *inherit.split('/')) | 
| + parser = ConfigParser.SafeConfigParser() | 
| + parser.read(filename) | 
| - def sections(self): | 
| - result = set() | 
| - for config in self.chain: | 
| - for section in config.sections(): | 
| - result.add(section) | 
| - return list(result) | 
| + def _apply_diff(self, section, option, value): | 
| + is_addition = option.endswith('+') | 
| + is_diff = is_addition or option.endswith('-') | 
| - def has_section(self, section): | 
| - for config in self.chain: | 
| - if config.has_section(section): | 
| - return True | 
| - return False | 
| + if is_diff: | 
| + option = option[:-1].rstrip() | 
| + try: | 
| + orig_value = self.get(section, option) | 
| + except ConfigParser.NoOptionError: | 
| 
 
Wladimir Palant
2015/06/25 14:11:40
What about ConfigParser.NoSectionError?
 
Sebastian Noack
2015/06/25 16:06:28
We already make sure to create the section if it d
 
 | 
| + raise DiffForUnknownOptionError(option, section) | 
| - def options(self, section): | 
| - result = set() | 
| - for config in self.chain: | 
| - if config.has_section(section): | 
| - for option in config.options(section): | 
| - result.add(option) | 
| - return list(result) | 
| + orig_values = orig_value.split() | 
| + diff_values = value.split() | 
| - def has_option(self, section, option): | 
| - for config in self.chain: | 
| - if config.has_section(section) and config.has_option(section, option): | 
| - return True | 
| - return False | 
| + if is_addition: | 
| + new_values = orig_values + [v for v in diff_values if v not in orig_values] | 
| + else: | 
| + new_values = [v for v in orig_values if v not in diff_values] | 
| - def get(self, section, option): | 
| - for config in self.chain: | 
| - if config.has_section(section) and config.has_option(section, option): | 
| - return config.get(section, option) | 
| - raise ConfigParser.NoOptionError(option, section) | 
| + value = ' '.join(new_values) | 
| - def items(self, section): | 
| - seen = set() | 
| - result = [] | 
| - for config in self.chain: | 
| - if config.has_section(section): | 
| - for name, value in config.items(section): | 
| - if name not in seen: | 
| - seen.add(name) | 
| - result.append(Item(name, value, config.source_path)) | 
| - return result | 
| + return is_diff, option, value | 
| + | 
| + def _process_parsers(self, parsers): | 
| + for parser, filename in parsers: | 
| + for section in parser.sections(): | 
| + if not self.has_section(section): | 
| + try: | 
| + self.add_section(section) | 
| + except ValueError: | 
| + # add_section() hardcodes 'default' and raises a ValueError if | 
| + # you try to add a section called like that (case insensitive). | 
| + # This bug has been fixed in Python 3. | 
| + self._sections[section] = self._dict() | 
| + | 
| + for option, value in parser.items(section): | 
| + is_diff, option, value = self._apply_diff(section, option, value) | 
| + ConfigParser.SafeConfigParser.set(self, section, option, value) | 
| + | 
| + if not is_diff: | 
| + self._origin[(section, option)] = filename | 
| + | 
| + def read(self, filenames): | 
| + if isinstance(filenames, basestring): | 
| + filenames = [filenames] | 
| + | 
| + read_ok = [] | 
| + for filename in filenames: | 
| + parser = ConfigParser.SafeConfigParser() | 
| + read_ok.extend(parser.read(filename)) | 
| + self._process_parsers(self._get_parser_chain(parser, filename)) | 
| + | 
| + return read_ok | 
| + | 
| + def items(self, section, *args, **kwargs): | 
| + items = [] | 
| + for option, value in ConfigParser.SafeConfigParser.items(self, section, *args, **kwargs): | 
| + items.append(Item(option, value, self._origin[(section, option)])) | 
| + return items | 
| def option_source(self, section, option): | 
| - for config in self.chain: | 
| - if config.has_section(section) and config.has_option(section, option): | 
| - return config.source_path | 
| - raise ConfigParser.NoOptionError(option, section) | 
| + try: | 
| + return self._origin[(section, option)] | 
| + except KeyError: | 
| + if not self.has_section(section): | 
| + raise ConfigParser.NoSectionError(section) | 
| + raise ConfigParser.NoOptionError(option, section) | 
| + | 
| + def readfp(self, fp, filename=None): | 
| + raise NotImplementedError | 
| + | 
| + def set(self, section, option, value=None): | 
| + raise NotImplementedError | 
| + | 
| + def remove_option(self, section, option): | 
| + raise NotImplementedError | 
| + | 
| + def remove_section(self, section): | 
| + raise NotImplementedError |