Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: chainedconfigparser.py

Issue 29319007: Issue 2711 - Refactored ChainedConfigParser, allowing manipulation of list items (Closed)
Patch Set: Don't convert options to lowercase and decode config files as UTF-8 Created July 7, 2015, 1:06 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | packager.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
OLDNEW
« no previous file with comments | « no previous file | packager.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld