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):
17 def __init__(self, message, line_no, line):
20 self.line_no = line_no
23 return 'at line %d, %s: %r' % (self.line_no, self.msg, self.line)
26 class BaseParser(object):
28 PARSE_EXC = ParseError
31 super(BaseParser, self).__init__()
34 def _assignment(self, key, value):
35 self.assignment(key, value)
38 def _get_section(self, line):
39 if not line.endswith(']'):
40 return self.error_no_section_end_bracket(line)
42 return self.error_no_section_name(line)
46 def _split_key_value(self, line):
47 colon = line.find(':')
48 equal = line.find('=')
49 if colon < 0 and equal < 0:
50 return self.error_invalid_assignment(line)
52 if colon < 0 or (0 <= equal < colon):
53 key, value = line[:equal], line[equal + 1:]
55 key, value = line[:colon], line[colon + 1:]
58 if value and value[0] == value[-1] and value.startswith(("\"", "'")):
60 return key.strip(), [value]
62 def _single_line_parse(self, line, key, value):
65 if line.startswith(('#', ';')):
66 self.comment(line[1:].strip())
69 active, _, comment = line.partition(';')
70 self.comment(comment.strip())
73 # Blank line, ends multi-line values
75 key, value = self._assignment(key, value)
78 if active.startswith((' ', '\t')):
79 # Continuation of previous assignment
81 return self.error_unexpected_continuation(line)
83 value.append(active.lstrip())
87 # Flush previous assignment, if any
88 key, value = self._assignment(key, value)
90 if active.startswith('['):
92 section = self._get_section(active)
94 self.new_section(section)
97 key, value = self._split_key_value(active)
99 return self.error_empty_key(line)
103 def parse(self, line_iter=None):
104 if line_iter is None:
110 for line in line_iter:
111 key, value = self._single_line_parse(line, key, value)
114 # Flush previous assignment, if any
115 self._assignment(key, value)
117 def assignment(self, key, value):
118 """Called when a full assignment is parsed."""
119 raise NotImplementedError()
121 def new_section(self, section):
122 """Called when a new section is started."""
123 raise NotImplementedError()
125 def comment(self, comment):
126 """Called when a comment is parsed."""
129 def make_parser_error(self, template, line):
130 raise self.PARSE_EXC(template, self.line_no, line)
132 def error_invalid_assignment(self, line):
133 self.make_parser_error("No ':' or '=' found in assignment", line)
135 def error_empty_key(self, line):
136 self.make_parser_error('Key cannot be empty', line)
138 def error_unexpected_continuation(self, line):
139 self.make_parser_error('Unexpected continuation line', line)
141 def error_no_section_end_bracket(self, line):
142 self.make_parser_error('Invalid section (must end with ])', line)
144 def error_no_section_name(self, line):
145 self.make_parser_error('Empty section name', line)
148 class ConfigParser(BaseParser):
149 """Parses a single config file, populating 'sections' to look like:
151 {'DEFAULT': {'key': [value, ...], ...},
155 def __init__(self, filename, sections):
156 super(ConfigParser, self).__init__()
157 self.filename = filename
158 self.sections = sections
161 def parse(self, line_iter=None):
162 with open(self.filename) as f:
163 return super(ConfigParser, self).parse(f)
165 def new_section(self, section):
166 self.section = section
167 self.sections.setdefault(self.section, [])
169 def assignment(self, key, value):
171 raise self.error_no_section()
173 value = '\n'.join(value)
174 self.sections[self.section].append([key, value])
176 def error_no_section(self):
177 self.make_parser_error('Section must be started before assignment', '')