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