1 # Copyright 2012 OpenStack Foundation
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
4 # not use this file except in compliance with the License. You may obtain
5 # a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations
16 class ParseError(Exception):
18 def __init__(self, message, line_no, line):
21 self.line_no = line_no
24 return 'at line %d, %s: %r' % (self.line_no, self.msg, self.line)
27 class SectionParseError(ParseError):
32 class LineParser(object):
34 PARSE_EXC = ParseError
37 def strip_key_value(key, value):
40 if value and value[0] == value[-1] and value.startswith(('"', "'")):
44 def __init__(self, line, line_no):
45 super(LineParser, self).__init__()
47 self.line_no = line_no
48 self.continuation = line != line.lstrip()
49 semi_active, _, semi_comment = line.partition(';')
50 pound_active, _, pound_comment = line.partition('#')
51 if not semi_comment and not pound_comment:
52 self.active = line.strip()
54 elif len(semi_comment) > len(pound_comment):
55 self.active = semi_active.strip()
56 self.comment = semi_comment.strip()
58 self.active = pound_active.strip()
59 self.comment = pound_comment.strip()
60 self._section_name = None
63 template = "line %d: active '%s' comment '%s'\n%s"
64 return template % (self.line_no, self.active, self.comment, self.line)
67 def section_name(self):
68 if self._section_name is None:
69 if not self.active.startswith('['):
70 raise self.error_no_section_start_bracket()
71 if not self.active.endswith(']'):
72 raise self.error_no_section_end_bracket()
73 self._section_name = ''
75 self._section_name = self.active[1:-1]
76 if not self._section_name:
77 raise self.error_no_section_name()
78 return self._section_name
80 def is_active_line(self):
81 return bool(self.active)
83 def is_continuation(self):
84 return self.continuation
86 def split_key_value(self):
87 for sep in ['=', ':']:
88 words = self.active.split(sep, 1)
90 return self.strip_key_value(*words)
94 return self.active.rstrip(), '@'
96 def error_invalid_assignment(self):
97 return self.PARSE_EXC("No ':' or '=' found in assignment", self.line_no, self.line)
99 def error_empty_key(self):
100 return self.PARSE_EXC('Key cannot be empty', self.line_no, self.line)
102 def error_unexpected_continuation(self):
103 return self.PARSE_EXC('Unexpected continuation line', self.line_no, self.line)
105 def error_no_section_start_bracket(self):
106 return SectionParseError('Invalid section (must start with [)', self.line_no, self.line)
108 def error_no_section_end_bracket(self):
109 return self.PARSE_EXC('Invalid section (must end with ])', self.line_no, self.line)
111 def error_no_section_name(self):
112 return self.PARSE_EXC('Empty section name', self.line_no, self.line)
115 class BaseParser(object):
117 def parse(self, data=None):
119 return self._parse(data.splitlines())
121 def _next_key_value(self, line_parser, key, value):
122 self.comment(line_parser)
124 if not line_parser.is_active_line():
125 # Blank line, ends multi-line values
127 key, value = self.assignment(key, value, line_parser)
130 if line_parser.is_continuation():
131 # Continuation of previous assignment
133 raise line_parser.error_unexpected_continuation()
135 value.append(line_parser.active.lstrip())
139 # Flush previous assignment, if any
140 key, value = self.assignment(key, value, line_parser)
144 self.new_section(line_parser)
145 except SectionParseError:
150 key, value = line_parser.split_key_value()
152 raise line_parser.error_empty_key()
155 def _parse(self, line_iter):
159 parse_iter = (LineParser(line, line_no) for line_no, line in enumerate(line_iter))
160 for line_parser in parse_iter:
161 key, value = self._next_key_value(line_parser, key, value)
164 # Flush previous assignment, if any
165 self.assignment(key, value, LineParser('EOF', -1))
167 def _assignment(self, key, value, line_parser):
168 """Called when a full assignment is parsed."""
169 raise NotImplementedError()
171 def assignment(self, key, value, line_parser):
172 self._assignment(key, value, line_parser)
175 def new_section(self, line_parser):
176 """Called when a new section is started."""
177 raise NotImplementedError()
179 def comment(self, line_parser):
180 """Called when a comment is parsed."""
181 raise NotImplementedError()
184 class ConfigParser(BaseParser):
185 """Parses a single config file, populating 'sections' to look like:
191 ['key1', 'value1\nvalue2'],
192 ['key2', 'value3\nvalue4'],
198 ['key3', 'value5\nvalue6'],
204 def __init__(self, filename, sections=None):
205 super(ConfigParser, self).__init__()
206 self.filename = filename
207 if sections is not None:
208 self.sections = sections
211 self.section_name = None
214 def parse(self, data=None):
217 with open(data) as f:
218 return self._parse(f)
221 return iter(self.sections)
223 def find_section_index(self, section_name):
224 return next((i for i, (name, value) in enumerate(self) if name == section_name), -1)
226 def find_section(self, section_name):
227 return next((value for name, value in self.sections if name == section_name), None)
229 def new_section(self, line_parser):
230 section_name = line_parser.section_name
231 index = self.find_section_index(section_name)
232 self.section_name = section_name
234 self.section = [section_name, []]
235 self.sections.append(self.section)
237 self.section = self.sections[index]
239 def _assignment(self, key, value, line_parser):
240 if not self.section_name:
241 raise line_parser.error_no_section_name()
243 value = '\n'.join(value)
245 self.section[1].append(entry)
247 def comment(self, line_parser):
248 """Called when a comment is parsed."""