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
|
# Copyright 2012 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class ParseError(Exception):
def __init__(self, message, line_no, line):
self.msg = message
self.line = line
self.line_no = line_no
def __str__(self):
return 'at line %d, %s: %r' % (self.line_no, self.msg, self.line)
class SectionParseError(ParseError):
pass
class LineParser(object):
PARSE_EXC = ParseError
@staticmethod
def strip_key_value(key, value):
key = key.strip()
value = value.strip()
if value and value[0] == value[-1] and value.startswith(('"', "'")):
value = value[1:-1]
return key, [value]
def __init__(self, line, line_no):
super(LineParser, self).__init__()
self.line = line
self.line_no = line_no
self.continuation = line != line.lstrip()
semi_active, _, semi_comment = line.partition(';')
pound_active, _, pound_comment = line.partition('#')
if not semi_comment and not pound_comment:
self.active = line.strip()
self.comment = ''
elif len(semi_comment) > len(pound_comment):
self.active = semi_active.strip()
self.comment = semi_comment.strip()
else:
self.active = pound_active.strip()
self.comment = pound_comment.strip()
self._section_name = None
def __repr__(self):
template = "line %d: active '%s' comment '%s'\n%s"
return template % (self.line_no, self.active, self.comment, self.line)
@property
def section_name(self):
if self._section_name is None:
if not self.active.startswith('['):
raise self.error_no_section_start_bracket()
if not self.active.endswith(']'):
raise self.error_no_section_end_bracket()
self._section_name = ''
if self.active:
self._section_name = self.active[1:-1]
if not self._section_name:
raise self.error_no_section_name()
return self._section_name
def is_active_line(self):
return bool(self.active)
def is_continuation(self):
return self.continuation
def split_key_value(self):
for sep in ['=', ':']:
words = self.active.split(sep, 1)
try:
return self.strip_key_value(*words)
except TypeError:
pass
return self.active.rstrip(), '@'
def error_invalid_assignment(self):
return self.PARSE_EXC("No ':' or '=' found in assignment", self.line_no, self.line)
def error_empty_key(self):
return self.PARSE_EXC('Key cannot be empty', self.line_no, self.line)
def error_unexpected_continuation(self):
return self.PARSE_EXC('Unexpected continuation line', self.line_no, self.line)
def error_no_section_start_bracket(self):
return SectionParseError('Invalid section (must start with [)', self.line_no, self.line)
def error_no_section_end_bracket(self):
return self.PARSE_EXC('Invalid section (must end with ])', self.line_no, self.line)
def error_no_section_name(self):
return self.PARSE_EXC('Empty section name', self.line_no, self.line)
class BaseParser(object):
def parse(self, data=None):
if data is not None:
return self._parse(data.splitlines())
def _next_key_value(self, line_parser, key, value):
self.comment(line_parser)
if not line_parser.is_active_line():
# Blank line, ends multi-line values
if key:
key, value = self.assignment(key, value, line_parser)
return key, value
if line_parser.is_continuation():
# Continuation of previous assignment
if key is None:
raise line_parser.error_unexpected_continuation()
value.append(line_parser.active.lstrip())
return key, value
if key:
# Flush previous assignment, if any
key, value = self.assignment(key, value, line_parser)
try:
# Section start
self.new_section(line_parser)
except SectionParseError:
pass
else:
return key, value
key, value = line_parser.split_key_value()
if not key:
raise line_parser.error_empty_key()
return key, value
def _parse(self, line_iter):
key = None
value = []
parse_iter = (LineParser(line, line_no) for line_no, line in enumerate(line_iter))
for line_parser in parse_iter:
key, value = self._next_key_value(line_parser, key, value)
if key:
# Flush previous assignment, if any
self.assignment(key, value, LineParser('EOF', -1))
def _assignment(self, key, value, line_parser):
"""Called when a full assignment is parsed."""
raise NotImplementedError()
def assignment(self, key, value, line_parser):
self._assignment(key, value, line_parser)
return None, []
def new_section(self, line_parser):
"""Called when a new section is started."""
raise NotImplementedError()
def comment(self, line_parser):
"""Called when a comment is parsed."""
raise NotImplementedError()
class ConfigParser(BaseParser):
"""Parses a single config file, populating 'sections' to look like:
[
[
'section1',
[
['key1', 'value1\nvalue2'],
['key2', 'value3\nvalue4'],
],
],
[
'section2',
[
['key3', 'value5\nvalue6'],
],
],
]
"""
def __init__(self, filename, sections=None):
super(ConfigParser, self).__init__()
self.filename = filename
if sections is not None:
self.sections = sections
else:
self.sections = []
self.section_name = None
self.section = None
def parse(self, data=None):
if not data:
data = self.filename
with open(data) as f:
return self._parse(f)
def __iter__(self):
return iter(self.sections)
def find_section_index(self, section_name):
return next((i for i, (name, value) in enumerate(self) if name == section_name), -1)
def find_section(self, section_name):
return next((value for name, value in self.sections if name == section_name), None)
def new_section(self, line_parser):
section_name = line_parser.section_name
index = self.find_section_index(section_name)
self.section_name = section_name
if index == -1:
self.section = [section_name, []]
self.sections.append(self.section)
else:
self.section = self.sections[index]
def _assignment(self, key, value, line_parser):
if not self.section_name:
raise line_parser.error_no_section_name()
value = '\n'.join(value)
entry = [key, value]
self.section[1].append(entry)
def comment(self, line_parser):
"""Called when a comment is parsed."""
pass
|