| Left: | ||
| Right: |
| OLD | NEW |
|---|---|
| 1 # coding: utf-8 | 1 # coding: utf-8 |
| 2 | 2 |
| 3 # This Source Code Form is subject to the terms of the Mozilla Public | 3 # This Source Code Form is subject to the terms of the Mozilla Public |
| 4 # License, v. 2.0. If a copy of the MPL was not distributed with this | 4 # License, v. 2.0. If a copy of the MPL was not distributed with this |
| 5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. | 5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| 6 | 6 |
| 7 import os, codecs, ConfigParser | 7 import os |
| 8 import ConfigParser | |
| 8 | 9 |
| 9 class Item(tuple): | 10 class Item(tuple): |
| 10 def __new__(cls, name, value, source): | 11 def __new__(cls, name, value, source): |
| 11 result = super(Item, cls).__new__(cls, (name, value)) | 12 result = super(Item, cls).__new__(cls, (name, value)) |
| 12 result.source = source | 13 result.source = source |
| 13 return result | 14 return result |
| 14 | 15 |
| 15 class ChainedConfigParser: | 16 class DiffForUnknownOptionError(ConfigParser.Error): |
| 16 """ | 17 def __init__(self, option, section): |
| 18 ConfigParser.Error.__init__(self, 'Failed to apply diff for unknown option ' | |
| 19 '%r in section %r' % (option, section)) | |
| 20 self.option = option | |
| 21 self.section = section | |
| 22 self.args = (option, section) | |
| 23 | |
| 24 class ChainedConfigParser(ConfigParser.SafeConfigParser): | |
| 25 ''' | |
| 17 This class provides essentially the same interfaces as SafeConfigParser but | 26 This class provides essentially the same interfaces as SafeConfigParser but |
| 18 allows chaining configuration files so that one config file provides the | 27 allows chaining configuration files so that one config file provides the |
| 19 default values for the other. To specify the config file to inherit from | 28 default values for the other. To specify the config file to inherit from |
| 20 a config file needs to contain the following option: | 29 a config file needs to contain the following option: |
| 21 | 30 |
| 22 [default] | 31 [default] |
| 23 inherit = foo/bar.config | 32 inherit = foo/bar.config |
| 24 | 33 |
| 34 It is also possible to add values to or remove values from | |
| 35 whitespace-separated lists given by an inherited option: | |
| 36 | |
| 37 [section] | |
| 38 opt1 += foo | |
| 39 opt2 -= bar | |
| 40 | |
| 25 The value of the inherit option has to be a relative path with forward | 41 The value of the inherit option has to be a relative path with forward |
| 26 slashes as delimiters. Up to 5 configuration files can be chained this way, | 42 slashes as delimiters. Up to 5 configuration files can be chained this way, |
| 27 longer chains are disallowed to deal with circular references. | 43 longer chains are disallowed to deal with circular references. |
| 28 | 44 |
| 29 A main API difference to SafeConfigParser is the way a class instance is | 45 A main API difference to SafeConfigParser is the way a class instance is |
| 30 constructed: a file path has to be passed, this file is assumed to be | 46 constructed: a file path has to be passed, this file is assumed to be |
| 31 encoded as UTF-8. Also, ChainedConfigParser data is read-only and the | 47 encoded as UTF-8. Also, ChainedConfigParser data is read-only and the |
| 32 options are case-sensitive. An additional option_source(section, option) | 48 options are case-sensitive. An additional option_source(section, option) |
| 33 method is provided to get the path of the configuration file defining this | 49 method is provided to get the path of the configuration file defining this |
| 34 option (for relative paths). Items returned by the items() function also | 50 option (for relative paths). Items returned by the items() function also |
| 35 have a source attribute serving the same purpose. | 51 have a source attribute serving the same purpose. |
| 36 """ | 52 ''' |
| 37 | 53 |
| 38 def __init__(self, path): | 54 def __init__(self): |
| 39 self.chain = [] | 55 ConfigParser.SafeConfigParser.__init__(self) |
| 40 self.read_path(path) | 56 self._origin = {} |
| 41 | 57 |
| 42 def read_path(self, path): | 58 def _get_parser_chain(self, parser, filename): |
| 43 if len(self.chain) >= 5: | 59 parsers = [] |
| 44 raise Exception('Too much inheritance in config files') | |
| 45 | 60 |
| 46 config = ConfigParser.SafeConfigParser() | 61 while True: |
| 47 config.optionxform = str | 62 parsers.insert(0, (parser, filename)) |
| 48 config.source_path = path | |
| 49 handle = codecs.open(path, 'rb', encoding='utf-8') | |
| 50 config.readfp(handle) | |
| 51 handle.close() | |
| 52 self.chain.append(config) | |
| 53 | 63 |
| 54 if config.has_section('default') and config.has_option('default', 'inherit') : | 64 try: |
| 55 parts = config.get('default', 'inherit').split('/') | 65 inherit = parser.get('default', 'inherit') |
| 56 defaults_path = os.path.join(os.path.dirname(path), *parts) | 66 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): |
| 57 self.read_path(defaults_path) | 67 return parsers |
| 58 | 68 |
| 59 def defaults(self): | 69 filename = os.path.join(os.path.dirname(filename), *inherit.split('/')) |
| 60 result = {} | 70 parser = ConfigParser.SafeConfigParser() |
| 61 for config in reverse(self.chain): | 71 parser.read(filename) |
| 62 for key, value in config.defaults().iteritems(): | |
| 63 result[key] = value | |
| 64 return result | |
| 65 | 72 |
| 66 def sections(self): | 73 def _apply_diff(self, section, option, value): |
| 67 result = set() | 74 is_addition = option.endswith('+') |
| 68 for config in self.chain: | 75 is_diff = is_addition or option.endswith('-') |
| 69 for section in config.sections(): | |
| 70 result.add(section) | |
| 71 return list(result) | |
| 72 | 76 |
| 73 def has_section(self, section): | 77 if is_diff: |
| 74 for config in self.chain: | 78 option = option[:-1].rstrip() |
| 75 if config.has_section(section): | 79 try: |
| 76 return True | 80 orig_value = self.get(section, option) |
| 77 return False | 81 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
| |
| 82 raise DiffForUnknownOptionError(option, section) | |
| 78 | 83 |
| 79 def options(self, section): | 84 orig_values = orig_value.split() |
| 80 result = set() | 85 diff_values = value.split() |
| 81 for config in self.chain: | |
| 82 if config.has_section(section): | |
| 83 for option in config.options(section): | |
| 84 result.add(option) | |
| 85 return list(result) | |
| 86 | 86 |
| 87 def has_option(self, section, option): | 87 if is_addition: |
| 88 for config in self.chain: | 88 new_values = orig_values + [v for v in diff_values if v not in orig_valu es] |
| 89 if config.has_section(section) and config.has_option(section, option): | 89 else: |
| 90 return True | 90 new_values = [v for v in orig_values if v not in diff_values] |
| 91 return False | |
| 92 | 91 |
| 93 def get(self, section, option): | 92 value = ' '.join(new_values) |
| 94 for config in self.chain: | |
| 95 if config.has_section(section) and config.has_option(section, option): | |
| 96 return config.get(section, option) | |
| 97 raise ConfigParser.NoOptionError(option, section) | |
| 98 | 93 |
| 99 def items(self, section): | 94 return is_diff, option, value |
| 100 seen = set() | 95 |
| 101 result = [] | 96 def _process_parsers(self, parsers): |
| 102 for config in self.chain: | 97 for parser, filename in parsers: |
| 103 if config.has_section(section): | 98 for section in parser.sections(): |
| 104 for name, value in config.items(section): | 99 if not self.has_section(section): |
| 105 if name not in seen: | 100 try: |
| 106 seen.add(name) | 101 self.add_section(section) |
| 107 result.append(Item(name, value, config.source_path)) | 102 except ValueError: |
| 108 return result | 103 # add_section() hardcodes 'default' and raises a ValueError if |
| 104 # you try to add a section called like that (case insensitive). | |
| 105 # This bug has been fixed in Python 3. | |
| 106 self._sections[section] = self._dict() | |
| 107 | |
| 108 for option, value in parser.items(section): | |
| 109 is_diff, option, value = self._apply_diff(section, option, value) | |
| 110 ConfigParser.SafeConfigParser.set(self, section, option, value) | |
| 111 | |
| 112 if not is_diff: | |
| 113 self._origin[(section, option)] = filename | |
| 114 | |
| 115 def read(self, filenames): | |
| 116 if isinstance(filenames, basestring): | |
| 117 filenames = [filenames] | |
| 118 | |
| 119 read_ok = [] | |
| 120 for filename in filenames: | |
| 121 parser = ConfigParser.SafeConfigParser() | |
| 122 read_ok.extend(parser.read(filename)) | |
| 123 self._process_parsers(self._get_parser_chain(parser, filename)) | |
| 124 | |
| 125 return read_ok | |
| 126 | |
| 127 def items(self, section, *args, **kwargs): | |
| 128 items = [] | |
| 129 for option, value in ConfigParser.SafeConfigParser.items(self, section, *arg s, **kwargs): | |
| 130 items.append(Item(option, value, self._origin[(section, option)])) | |
| 131 return items | |
| 109 | 132 |
| 110 def option_source(self, section, option): | 133 def option_source(self, section, option): |
| 111 for config in self.chain: | 134 try: |
| 112 if config.has_section(section) and config.has_option(section, option): | 135 return self._origin[(section, option)] |
| 113 return config.source_path | 136 except KeyError: |
| 114 raise ConfigParser.NoOptionError(option, section) | 137 if not self.has_section(section): |
| 138 raise ConfigParser.NoSectionError(section) | |
| 139 raise ConfigParser.NoOptionError(option, section) | |
| 140 | |
| 141 def readfp(self, fp, filename=None): | |
| 142 raise NotImplementedError | |
| 143 | |
| 144 def set(self, section, option, value=None): | |
| 145 raise NotImplementedError | |
| 146 | |
| 147 def remove_option(self, section, option): | |
| 148 raise NotImplementedError | |
| 149 | |
| 150 def remove_section(self, section): | |
| 151 raise NotImplementedError | |
| OLD | NEW |