Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: chainedconfigparser.py

Issue 29319007: Issue 2711 - Refactored ChainedConfigParser, allowing manipulation of list items (Closed)
Patch Set: Addressed comments Created June 24, 2015, 8:49 a.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | packager.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« no previous file with comments | « no previous file | packager.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld