-
Notifications
You must be signed in to change notification settings - Fork 2k
/
config_tool.py
263 lines (224 loc) · 9.85 KB
/
config_tool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# encoding: utf-8
from __future__ import print_function
import six
import re
INSERT_NEW_SECTIONS_BEFORE_SECTION = 'app:main'
def config_edit_using_option_strings(config_filepath, desired_option_strings,
section, edit=False):
'''Writes the desired_option_strings to the config file.'''
# Parse the desired_options
desired_options = [parse_option_string(section, desired_option_string,
raise_on_error=True)
for desired_option_string in desired_option_strings]
# Make the changes
config_edit(config_filepath, desired_options, edit=edit)
def config_edit_using_merge_file(config_filepath, merge_config_filepath):
'''Merges options found in a config file (merge_config_filepath) into the
main config file (config_filepath).
'''
# Read and parse the merge config filepath
with open(merge_config_filepath, 'rb') as f:
input_lines = [six.ensure_str(line).rstrip('\n') for line in f]
desired_options_dict = parse_config(input_lines)
desired_options = desired_options_dict.values()
# Make the changes
config_edit(config_filepath, desired_options)
def config_edit(config_filepath, desired_options, edit=False):
'''Writes the desired_options to the config file.'''
# Read and parse the existing config file
with open(config_filepath, 'rb') as f:
input_lines = [six.ensure_str(line).rstrip('\n') for line in f]
existing_options_dict = parse_config(input_lines)
existing_options = existing_options_dict.values()
# For every desired option, decide what action to take
new_sections = calculate_new_sections(existing_options, desired_options)
changes = calculate_changes(existing_options_dict, desired_options, edit)
# write the file with the changes
output = make_changes(input_lines, new_sections, changes)
with open(config_filepath, 'wb') as f:
f.write(six.ensure_binary('\n'.join(output) + '\n'))
def parse_option_string(section, option_string, raise_on_error=False):
option_match = OPTION_RE.match(option_string)
if not option_match:
if raise_on_error:
raise ConfigToolError('Option did not parse: "%s". Must be: '
'"key = value"' % option_string)
return
is_commented_out, key, value = option_match.group('commentedout',
'option', 'value')
key = key.strip()
value = value.strip()
return Option(section, key, value, is_commented_out,
original=option_string)
class Option(object):
def __init__(self, section, key, value, is_commented_out, original=None):
self.section = section
self.key = key
self.value = value
self.is_commented_out = bool(is_commented_out)
self.original = original
def __repr__(self):
return '<Option [%s] %s>' % (self.section, self)
def __str__(self):
if self.original:
return self.original
return '%s%s = %s' % ('#' if self.is_commented_out else '',
self.key, self.value)
@property
def id(self):
return '%s-%s' % (self.section, self.key)
def comment_out(self):
self.is_commented_out = True
self.original = None # it is no longer accurate
def calculate_new_sections(existing_options, desired_options):
existing_sections = {option.section for option in existing_options}
desired_sections = {option.section for option in desired_options}
new_sections = desired_sections - existing_sections
return new_sections
class Changes(dict):
'''A store of Options that are to "edit" or "add" to existing sections of a
config file. (Excludes options that go into new sections.)'''
def add(self, action, option):
assert action in ('edit', 'add')
assert isinstance(option, Option)
if option.section not in self:
self[option.section] = {}
if not self[option.section].get(action):
self[option.section][action] = []
self[option.section][action].append(option)
def get(self, section, action):
try:
return self[section][action]
except KeyError:
return []
def calculate_changes(existing_options_dict, desired_options, edit):
changes = Changes()
for desired_option in desired_options:
action = 'edit' if desired_option.id in existing_options_dict \
else 'add'
if edit and action != 'edit':
raise ConfigToolError(
'Key "%s" does not exist in section "%s"' %
(desired_option.key, desired_option.section))
changes.add(action, desired_option)
return changes
def parse_config(input_lines):
'''
Returns a dict of Option objects, keyed by Option.id, given the lines in a
config file.
(Not using ConfigParser.set() as it does not store all the comments and
ordering)
'''
section = 'app:main' # default (for merge config files)
options = {}
for line in input_lines:
# ignore blank lines
if line.strip() == '':
continue
# section heading
section_match = SECTION_RE.match(line)
if section_match:
section = section_match.group('header')
continue
# option
option = parse_option_string(section, line)
if option:
options[option.id] = option
return options
def make_changes(input_lines, new_sections, changes):
'''Makes changes to the config file (returned as lines).'''
output = []
section = None
options_to_edit_in_this_section = {} # key: option
options_already_edited = set()
have_inserted_new_sections = False
def write_option(option):
output.append(str(option))
def insert_new_sections(new_sections):
for section in new_sections:
output.append('[%s]' % section)
for option in changes.get(section, 'add'):
write_option(option)
write_option('')
print('Created option %s = "%s" (NEW section "%s")' %
(option.key, option.value, section))
for line in input_lines:
# leave blank lines alone
if line.strip() == '':
output.append(line)
continue
section_match = SECTION_RE.match(line)
if section_match:
section = section_match.group('header')
if section == INSERT_NEW_SECTIONS_BEFORE_SECTION:
# insert new sections here
insert_new_sections(new_sections)
have_inserted_new_sections = True
output.append(line)
# at start of new section, write the 'add'ed options
for option in changes.get(section, 'add'):
write_option(option)
options_to_edit_in_this_section = {option.key: option
for option
in changes.get(section, 'edit')}
continue
existing_option = parse_option_string(section, line)
if not existing_option:
# leave alone comments (does not include commented options)
output.append(line)
continue
updated_option = \
options_to_edit_in_this_section.get(existing_option.key)
if updated_option:
changes_made = None
key = existing_option.key
if existing_option.id in options_already_edited:
if not existing_option.is_commented_out:
print('Commented out repeat of %s (section "%s")' %
(key, section))
existing_option.comment_out()
else:
print('Left commented out repeat of %s (section "%s")' %
(key, section))
elif not existing_option.is_commented_out and \
updated_option.is_commented_out:
changes_made = 'Commented out %s (section "%s")' % \
(key, section)
elif existing_option.is_commented_out and \
not updated_option.is_commented_out:
changes_made = 'Option uncommented and set %s = "%s" ' \
'(section "%s")' % \
(key, updated_option.value, section)
elif not existing_option.is_commented_out and \
not updated_option.is_commented_out:
if existing_option.value != updated_option.value:
changes_made = 'Edited option %s = "%s"->"%s" ' \
'(section "%s")' % \
(key, existing_option.value,
updated_option.value, section)
else:
changes_made = 'Option unchanged %s = "%s" ' \
'(section "%s")' % \
(key, existing_option.value, section)
if changes_made:
print(changes_made)
write_option(updated_option)
options_already_edited.add(updated_option.id)
else:
write_option(existing_option)
else:
write_option(existing_option)
if new_sections and not have_inserted_new_sections:
# must not have found the INSERT_NEW_SECTIONS_BEFORE_SECTION
# section so put the new sections at the end
insert_new_sections(new_sections)
return output
# Regexes basically the same as in ConfigParser - OPTCRE & SECTCRE
# Expressing them here because they move between Python 2 and 3
OPTION_RE = re.compile(r'(?P<commentedout>[#;]\s*)?' # custom
r'(?P<option>[^:=\s][^:=]*)'
r'\s*(?P<vi>[:=])\s*'
r'(?P<value>.*)$')
SECTION_RE = re.compile(r'\[(?P<header>.+)\]')
class ConfigToolError(Exception):
pass