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