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, lineno, line):
23 return 'at line %d, %s: %r' % (self.lineno, self.msg, self.line)
26 class BaseParser(object):
28 parse_exc = ParseError
30 def _assignment(self, key, value):
31 self.assignment(key, value)
34 def _get_section(self, line):
35 if not line.endswith(']'):
36 return self.error_no_section_end_bracket(line)
38 return self.error_no_section_name(line)
42 def _split_key_value(self, line):
43 colon = line.find(':')
44 equal = line.find('=')
45 if colon < 0 and equal < 0:
46 return line.strip(), '@'
48 if colon < 0 or (equal >= 0 and equal < colon):
49 key, value = line[:equal], line[equal + 1:]
51 key, value = line[:colon], line[colon + 1:]
54 if value and value[0] == value[-1] and value.startswith(("\"", "'")):
56 return key.strip(), [value]
58 def parse(self, lineiter):
66 lines = line.split(';')
69 # Blank line, ends multi-line values
71 key, value = self._assignment(key, value)
73 elif line.startswith((' ', '\t')):
74 # Continuation of previous assignment
76 self.error_unexpected_continuation(line)
78 value.append(line.lstrip())
82 # Flush previous assignment, if any
83 key, value = self._assignment(key, value)
85 if line.startswith('['):
87 section = self._get_section(line)
89 self.new_section(section)
90 elif line.startswith(('#', ';')):
91 self.comment(line[1:].lstrip())
93 key, value = self._split_key_value(line)
95 return self.error_empty_key(line)
98 # Flush previous assignment, if any
99 self._assignment(key, value)
101 def assignment(self, key, value):
102 """Called when a full assignment is parsed."""
103 raise NotImplementedError()
105 def new_section(self, section):
106 """Called when a new section is started."""
107 raise NotImplementedError()
109 def comment(self, comment):
110 """Called when a comment is parsed."""
113 def error_invalid_assignment(self, line):
114 raise self.parse_exc("No ':' or '=' found in assignment",
117 def error_empty_key(self, line):
118 raise self.parse_exc('Key cannot be empty', self.lineno, line)
120 def error_unexpected_continuation(self, line):
121 raise self.parse_exc('Unexpected continuation line',
124 def error_no_section_end_bracket(self, line):
125 raise self.parse_exc('Invalid section (must end with ])',
128 def error_no_section_name(self, line):
129 raise self.parse_exc('Empty section name', self.lineno, line)
132 class ConfigParser(BaseParser):
133 """Parses a single config file, populating 'sections' to look like:
135 {'DEFAULT': {'key': [value, ...], ...},
139 def __init__(self, filename, sections):
140 super(ConfigParser, self).__init__()
141 self.filename = filename
142 self.sections = sections
146 with open(self.filename) as f:
147 return super(ConfigParser, self).parse(f)
149 def find_section(self, sections, section):
150 return next((i for i, sect in enumerate(sections) if sect == section), -1)
152 def new_section(self, section):
153 self.section = section
154 index = self.find_section(self.sections, section)
156 self.sections.append([section, []])
158 def assignment(self, key, value):
160 raise self.error_no_section()
162 value = '\n'.join(value)
164 def append(sections, section):
166 index = self.find_section(sections, section)
167 sections[index][1].append(entry)
169 append(self.sections, self.section)
171 def parse_exc(self, msg, lineno, line=None):
172 return ParseError(msg, lineno, line)
174 def error_no_section(self):
175 return self.parse_exc('Section must be started before assignment',