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

Delta Between Two Patch Sets: chainedconfigparser.py

Issue 29319007: Issue 2711 - Refactored ChainedConfigParser, allowing manipulation of list items (Closed)
Left Patch Set: Created June 22, 2015, 9:58 p.m.
Right Patch Set: Addressed comments Created July 7, 2015, 3:18 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | packager.py » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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
LEFTRIGHT
« no previous file | packager.py » ('j') | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

Powered by Google App Engine
This is Rietveld