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 re | |
9 import ConfigParser | |
8 | 10 |
9 class Item(tuple): | 11 class Item(tuple): |
10 def __new__(cls, name, value, source): | 12 def __new__(cls, name, value, source): |
11 result = super(Item, cls).__new__(cls, (name, value)) | 13 result = super(Item, cls).__new__(cls, (name, value)) |
12 result.source = source | 14 result.source = source |
13 return result | 15 return result |
14 | 16 |
15 class ChainedConfigParser: | 17 class ChainedConfigParser(ConfigParser.SafeConfigParser): |
16 """ | 18 ''' |
17 This class provides essentially the same interfaces as SafeConfigParser but | 19 This class provides essentially the same interfaces as SafeConfigParser but |
18 allows chaining configuration files so that one config file provides the | 20 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 | 21 default values for the other. To specify the config file to inherit from |
20 a config file needs to contain the following option: | 22 a config file needs to contain the following option: |
21 | 23 |
22 [default] | 24 [default] |
23 inherit = foo/bar.config | 25 inherit = foo/bar.config |
24 | 26 |
25 The value of the inherit option has to be a relative path with forward | 27 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, | 28 slashes as delimiters. Up to 5 configuration files can be chained this way, |
27 longer chains are disallowed to deal with circular references. | 29 longer chains are disallowed to deal with circular references. |
28 | 30 |
29 A main API difference to SafeConfigParser is the way a class instance is | 31 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 | 32 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 | 33 encoded as UTF-8. Also, ChainedConfigParser data is read-only and the |
32 options are case-sensitive. An additional option_source(section, option) | 34 options are case-sensitive. An additional option_source(section, option) |
33 method is provided to get the path of the configuration file defining this | 35 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 | 36 option (for relative paths). Items returned by the items() function also |
35 have a source attribute 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.
| |
36 """ | 38 ''' |
37 | 39 |
38 def __init__(self, path): | 40 def __init__(self): |
39 self.chain = [] | 41 ConfigParser.SafeConfigParser.__init__(self) |
40 self.read_path(path) | 42 self._origin = {} |
41 | 43 |
42 def read_path(self, path): | 44 def _get_parser_chain(self, file, filename): |
43 if len(self.chain) >= 5: | 45 parsers = [] |
44 raise Exception('Too much inheritance in config files') | |
45 | 46 |
46 config = ConfigParser.SafeConfigParser() | 47 parser = ConfigParser.SafeConfigParser() |
47 config.optionxform = str | 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
| |
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 | 49 |
54 if config.has_section('default') and config.has_option('default', 'inherit') : | 50 while True: |
55 parts = config.get('default', 'inherit').split('/') | 51 parsers.insert(0, (parser, filename)) |
56 defaults_path = os.path.join(os.path.dirname(path), *parts) | |
57 self.read_path(defaults_path) | |
58 | 52 |
59 def defaults(self): | 53 try: |
60 result = {} | 54 inherit = parser.get('default', 'inherit') |
61 for config in reverse(self.chain): | 55 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): |
62 for key, value in config.defaults().iteritems(): | 56 return parsers |
63 result[key] = value | |
64 return result | |
65 | 57 |
66 def sections(self): | 58 filename = os.path.join(os.path.dirname(filename), *inherit.split('/')) |
67 result = set() | 59 parser = ConfigParser.SafeConfigParser() |
68 for config in self.chain: | 60 parser.read(filename) |
69 for section in config.sections(): | |
70 result.add(section) | |
71 return list(result) | |
72 | 61 |
73 def has_section(self, section): | 62 def _apply_diff(self, section, option, value): |
74 for config in self.chain: | 63 addition = option.endswith('+') |
75 if config.has_section(section): | 64 removal = option.endswith('-') |
76 return True | |
77 return False | |
78 | 65 |
79 def options(self, section): | 66 if addition or removal: |
80 result = set() | 67 option = option[:-1].rstrip() |
81 for config in self.chain: | 68 old_value = self.get(section, option) |
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.
| |
82 if config.has_section(section): | |
83 for option in config.options(section): | |
84 result.add(option) | |
85 return list(result) | |
86 | 69 |
87 def has_option(self, section, option): | 70 if addition: |
88 for config in self.chain: | 71 value = '%s %s' % (old_value, value) |
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.
| |
89 if config.has_section(section) and config.has_option(section, option): | 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.
| |
90 return True | 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.
| |
91 return False | |
92 | 74 |
93 def get(self, section, option): | 75 return option, value |
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 | 76 |
99 def items(self, section): | 77 def _read(self, file, filename): |
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
| |
100 seen = set() | 78 parsers = self._get_parser_chain(file, filename) |
101 result = [] | 79 |
102 for config in self.chain: | 80 for parser, filename in parsers: |
103 if config.has_section(section): | 81 for section in parser.sections(): |
104 for name, value in config.items(section): | 82 for option, value in parser.items(section): |
105 if name not in seen: | 83 option, value = self._apply_diff(section, option, value) |
106 seen.add(name) | 84 try: |
107 result.append(Item(name, value, config.source_path)) | 85 self.set(section, option, value) |
108 return result | 86 except ConfigParser.NoSectionError: |
87 try: | |
88 self.add_section(section) | |
89 except ValueError: | |
90 # add_section() hardcodes 'default' and raises a ValueError if | |
91 # you try to add a section called like that (case insensitive). | |
92 # This bug has been fixed in Python 3. | |
93 self._sections[section] = self._dict() | |
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) | |
95 self._origin[(section, option)] = filename | |
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
| |
96 | |
97 def items(self, section, *args, **kwargs): | |
98 items = [] | |
99 for option, value in ConfigParser.SafeConfigParser.items(self, section, *arg s, **kwargs): | |
100 items.append(Item(option, value, self._origin[(section, option)])) | |
101 return items | |
109 | 102 |
110 def option_source(self, section, option): | 103 def option_source(self, section, option): |
111 for config in self.chain: | 104 try: |
112 if config.has_section(section) and config.has_option(section, option): | 105 return self._origin[(section, option)] |
113 return config.source_path | 106 except KeyError: |
114 raise ConfigParser.NoOptionError(option, section) | 107 raise ConfigParser.NoOptionError(option, section) |
OLD | NEW |