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 July 7, 2015, 3:18 p.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,10 @@
# 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 io
+import ConfigParser
+from StringIO import StringIO
class Item(tuple):
def __new__(cls, name, value, source):
@@ -12,8 +15,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,93 +33,138 @@
[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.
- A main API difference to SafeConfigParser is the way a class instance is
- constructed: a file path has to be passed, this file is assumed to be
- encoded as UTF-8. Also, ChainedConfigParser data is read-only and the
- options are case-sensitive. An additional option_source(section, option)
- 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.
- """
+ As opposed to SafeConfigParser, files are decoded as UTF-8 while
+ reading. Also, ChainedConfigParser data is read-only. An additional
+ option_source(section, option) 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 _make_parser(self, filename):
+ parser = ConfigParser.SafeConfigParser()
+ parser.optionxform = lambda option: option
- 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)
+ with io.open(filename, encoding='utf-8') as file:
+ parser.readfp(file, 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)
+ return parser
- def defaults(self):
- result = {}
- for config in reverse(self.chain):
- for key, value in config.defaults().iteritems():
- result[key] = value
- return result
+ def _get_parser_chain(self, parser, filename):
+ parsers = []
- def sections(self):
- result = set()
- for config in self.chain:
- for section in config.sections():
- result.add(section)
- return list(result)
+ while True:
+ parsers.insert(0, (parser, filename))
- def has_section(self, section):
- for config in self.chain:
- if config.has_section(section):
- return True
- return False
+ try:
+ inherit = parser.get('default', 'inherit')
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ return parsers
- 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)
+ filename = os.path.join(os.path.dirname(filename), *inherit.split('/'))
+ parser = self._make_parser(filename)
- 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
+ def _apply_diff(self, section, option, value):
+ is_addition = option.endswith('+')
+ is_diff = is_addition or option.endswith('-')
- 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)
+ if is_diff:
+ option = option[:-1].rstrip()
+ try:
+ orig_value = self.get(section, option)
+ except ConfigParser.NoOptionError:
+ raise DiffForUnknownOptionError(option, section)
- 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
+ orig_values = orig_value.split()
+ diff_values = value.split()
+
+ 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]
+
+ value = ' '.join(new_values)
+
+ 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, self.optionxform(option))] = filename
+
+ def read(self, filenames):
+ if isinstance(filenames, basestring):
+ filenames = [filenames]
+
+ read_ok = []
+ for filename in filenames:
+ try:
+ parser = self._make_parser(filename)
+ except IOError:
+ continue
+ self._process_parsers(self._get_parser_chain(parser, filename))
+ read_ok.append(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, self.optionxform(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)
+ option = self.optionxform(option)
+ 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
« no previous file with comments | « no previous file | packager.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld