Left: | ||
Right: |
LEFT | RIGHT |
---|---|
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 | 7 import os |
8 import re | 8 import io |
9 import ConfigParser | 9 import ConfigParser |
10 from StringIO import StringIO | |
10 | 11 |
11 class Item(tuple): | 12 class Item(tuple): |
12 def __new__(cls, name, value, source): | 13 def __new__(cls, name, value, source): |
13 result = super(Item, cls).__new__(cls, (name, value)) | 14 result = super(Item, cls).__new__(cls, (name, value)) |
14 result.source = source | 15 result.source = source |
15 return result | 16 return result |
17 | |
18 class DiffForUnknownOptionError(ConfigParser.Error): | |
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) | |
16 | 25 |
17 class ChainedConfigParser(ConfigParser.SafeConfigParser): | 26 class ChainedConfigParser(ConfigParser.SafeConfigParser): |
18 ''' | 27 ''' |
19 This class provides essentially the same interfaces as SafeConfigParser but | 28 This class provides essentially the same interfaces as SafeConfigParser but |
20 allows chaining configuration files so that one config file provides the | 29 allows chaining configuration files so that one config file provides the |
21 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 |
22 a config file needs to contain the following option: | 31 a config file needs to contain the following option: |
23 | 32 |
24 [default] | 33 [default] |
25 inherit = foo/bar.config | 34 inherit = foo/bar.config |
26 | 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 | |
27 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 |
28 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, |
29 longer chains are disallowed to deal with circular references. | 45 longer chains are disallowed to deal with circular references. |
30 | 46 |
31 A main API difference to SafeConfigParser is the way a class instance is | 47 As opposed to SafeConfigParser, files are decoded as UTF-8 while |
32 constructed: a file path has to be passed, this file is assumed to be | 48 reading. Also, ChainedConfigParser data is read-only. An additional |
33 encoded as UTF-8. Also, ChainedConfigParser data is read-only and the | 49 option_source(section, option) method is provided to get the path |
34 options are case-sensitive. An additional option_source(section, option) | 50 of the configuration file defining this option (for relative paths). |
35 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 |
36 option (for relative paths). Items returned by the items() function also | 52 serving the same purpose. |
37 have a source attribute serving the same purpose. | |
Wladimir Palant
2015/06/23 09:43:36
Extend documentation to mention the new += and -=
Sebastian Noack
2015/06/24 08:51:09
Done.
| |
38 ''' | 53 ''' |
39 | 54 |
40 def __init__(self): | 55 def __init__(self): |
41 ConfigParser.SafeConfigParser.__init__(self) | 56 ConfigParser.SafeConfigParser.__init__(self) |
42 self._origin = {} | 57 self._origin = {} |
43 | 58 |
44 def _get_parser_chain(self, file, filename): | 59 def _make_parser(self, filename): |
60 parser = ConfigParser.SafeConfigParser() | |
61 parser.optionxform = lambda option: option | |
62 | |
63 with io.open(filename, encoding='utf-8') as file: | |
64 parser.readfp(file, filename) | |
65 | |
66 return parser | |
67 | |
68 def _get_parser_chain(self, parser, filename): | |
45 parsers = [] | 69 parsers = [] |
46 | |
47 parser = ConfigParser.SafeConfigParser() | |
48 parser._read(file, filename) | |
Wladimir Palant
2015/06/23 09:43:35
Please use public API here:
parser.readfp(file,
Sebastian Noack
2015/06/24 08:51:10
This code has been gone while addressing the comme
| |
49 | 70 |
50 while True: | 71 while True: |
51 parsers.insert(0, (parser, filename)) | 72 parsers.insert(0, (parser, filename)) |
52 | 73 |
53 try: | 74 try: |
54 inherit = parser.get('default', 'inherit') | 75 inherit = parser.get('default', 'inherit') |
55 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): | 76 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): |
56 return parsers | 77 return parsers |
57 | 78 |
58 filename = os.path.join(os.path.dirname(filename), *inherit.split('/')) | 79 filename = os.path.join(os.path.dirname(filename), *inherit.split('/')) |
59 parser = ConfigParser.SafeConfigParser() | 80 parser = self._make_parser(filename) |
60 parser.read(filename) | |
61 | 81 |
62 def _apply_diff(self, section, option, value): | 82 def _apply_diff(self, section, option, value): |
63 addition = option.endswith('+') | 83 is_addition = option.endswith('+') |
64 removal = option.endswith('-') | 84 is_diff = is_addition or option.endswith('-') |
65 | 85 |
66 if addition or removal: | 86 if is_diff: |
67 option = option[:-1].rstrip() | 87 option = option[:-1].rstrip() |
68 old_value = self.get(section, option) | 88 try: |
Wladimir Palant
2015/06/23 09:43:36
This will throw when trying to change an option th
Sebastian Noack
2015/06/24 08:51:09
Done.
| |
89 orig_value = self.get(section, option) | |
90 except ConfigParser.NoOptionError: | |
91 raise DiffForUnknownOptionError(option, section) | |
69 | 92 |
70 if addition: | 93 orig_values = orig_value.split() |
71 value = '%s %s' % (old_value, value) | 94 diff_values = value.split() |
Wladimir Palant
2015/06/23 09:43:35
I don't think that we want duplicate values - we a
Sebastian Noack
2015/06/24 08:51:10
Done.
| |
72 elif removal: | |
Wladimir Palant
2015/06/23 09:43:35
Nit: no need for elif, can be simply else. But I d
Sebastian Noack
2015/06/24 08:51:09
Done.
| |
73 value = re.sub(r'\b(?:%s)\b\s*' % '|'.join(map(re.escape, value.split()) ), '', old_value).rstrip() | |
Wladimir Palant
2015/06/23 09:43:35
I don't think that regular expressions are the rig
Sebastian Noack
2015/06/24 08:51:09
Done.
| |
74 | 95 |
75 return option, value | 96 if is_addition: |
97 new_values = orig_values + [v for v in diff_values if v not in orig_valu es] | |
98 else: | |
99 new_values = [v for v in orig_values if v not in diff_values] | |
76 | 100 |
77 def _read(self, file, filename): | 101 value = ' '.join(new_values) |
Wladimir Palant
2015/06/23 09:43:35
Is it really a good idea to override private metho
Sebastian Noack
2015/06/24 08:51:10
Overriding read() and readfp() would require quite
| |
78 parsers = self._get_parser_chain(file, filename) | |
79 | 102 |
103 return is_diff, option, value | |
104 | |
105 def _process_parsers(self, parsers): | |
80 for parser, filename in parsers: | 106 for parser, filename in parsers: |
81 for section in parser.sections(): | 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 | |
82 for option, value in parser.items(section): | 117 for option, value in parser.items(section): |
83 option, value = self._apply_diff(section, option, value) | 118 is_diff, option, value = self._apply_diff(section, option, value) |
84 try: | 119 ConfigParser.SafeConfigParser.set(self, section, option, value) |
85 self.set(section, option, value) | 120 |
86 except ConfigParser.NoSectionError: | 121 if not is_diff: |
87 try: | 122 self._origin[(section, self.optionxform(option))] = filename |
88 self.add_section(section) | 123 |
89 except ValueError: | 124 def read(self, filenames): |
90 # add_section() hardcodes 'default' and raises a ValueError if | 125 if isinstance(filenames, basestring): |
91 # you try to add a section called like that (case insensitive). | 126 filenames = [filenames] |
92 # This bug has been fixed in Python 3. | 127 |
93 self._sections[section] = self._dict() | 128 read_ok = [] |
Wladimir Palant
2015/06/23 09:43:36
I cannot say that I like this hack. How about simp
Sebastian Noack
2015/06/24 08:51:09
"default" != "DEFAULT". The latter is handled spec
Wladimir Palant
2015/06/25 14:11:40
I see. Still, should we access private variables i
Sebastian Noack
2015/06/25 16:06:28
The except block will only be reached in Python ve
Wladimir Palant
2015/06/25 16:12:28
That is: every Python version we support. And I'm
Sebastian Noack
2015/06/25 23:05:31
In case I didn't stress this enough, this is a bug
Wladimir Palant
2015/06/26 13:17:21
It doesn't matter what you call it - it's document
Sebastian Noack
2015/06/26 13:37:53
So your concerns here are only about using interna
| |
94 self.set(section, option, value) | 129 for filename in filenames: |
95 self._origin[(section, option)] = filename | 130 try: |
Wladimir Palant
2015/06/23 09:43:35
Ok, we have a problem right here... Consider the f
Sebastian Noack
2015/06/24 08:51:09
I first thought that we can simply rely on the sou
| |
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 | |
96 | 138 |
97 def items(self, section, *args, **kwargs): | 139 def items(self, section, *args, **kwargs): |
98 items = [] | 140 items = [] |
99 for option, value in ConfigParser.SafeConfigParser.items(self, section, *arg s, **kwargs): | 141 for option, value in ConfigParser.SafeConfigParser.items(self, section, *arg s, **kwargs): |
100 items.append(Item(option, value, self._origin[(section, option)])) | 142 items.append(Item( |
143 option, value, | |
144 self._origin[(section, self.optionxform(option))] | |
145 )) | |
101 return items | 146 return items |
102 | 147 |
103 def option_source(self, section, option): | 148 def option_source(self, section, option): |
149 option = self.optionxform(option) | |
104 try: | 150 try: |
105 return self._origin[(section, option)] | 151 return self._origin[(section, option)] |
106 except KeyError: | 152 except KeyError: |
153 if not self.has_section(section): | |
154 raise ConfigParser.NoSectionError(section) | |
107 raise ConfigParser.NoOptionError(option, section) | 155 raise ConfigParser.NoOptionError(option, section) |
156 | |
157 def readfp(self, fp, filename=None): | |
158 raise NotImplementedError | |
159 | |
160 def set(self, section, option, value=None): | |
161 raise NotImplementedError | |
162 | |
163 def add_section(self, section): | |
164 raise NotImplementedError | |
165 | |
166 def remove_option(self, section, option): | |
167 raise NotImplementedError | |
168 | |
169 def remove_section(self, section): | |
170 raise NotImplementedError | |
LEFT | RIGHT |