| Index: chainedconfigparser.py |
| =================================================================== |
| --- a/chainedconfigparser.py |
| +++ b/chainedconfigparser.py |
| @@ -4,7 +4,9 @@ |
| # 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 |
| +from StringIO import StringIO |
| class Item(tuple): |
| def __new__(cls, name, value, source): |
| @@ -12,8 +14,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 +32,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 +50,106 @@ |
| 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: |
| + 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: |
| + ConfigParser.SafeConfigParser.add_section(self, 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. |
| + ConfigParser.SafeConfigParser.readfp(self, StringIO('[%s]' % section)) |
| + |
| + 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 add_section(self, section): |
| + raise NotImplementedError |
| + |
| + def remove_option(self, section, option): |
| + raise NotImplementedError |
| + |
| + def remove_section(self, section): |
| + raise NotImplementedError |