0001"""
0002A parser for .ini-syntax files. INIParser should be subclassed to
0003create a data structure from the file. See INIParser for more
0004"""
0005
0006class ParseError(Exception):
0007
0008 def __init__(self, message, filename=None, lineno=None, column=None, line=None):
0009
0010
0011
0012
0013
0014 self.message = message
0015 self.filename = filename
0016 self.lineno = lineno
0017 self.column = column
0018 self.line = line
0019
0020 def __str__(self):
0021 msg = self.message
0022 if self.filename and self.lineno:
0023 msg += ' in %s:%s' % (self.filename, self.lineno)
0024 elif self.filename:
0025 msg += ' in %s' % self.filename
0026 elif self.lineno:
0027 msg += ' at line %s' % self.lineno
0028 return msg
0029
0030class INIParser(object):
0031
0032 """
0033 A parser for .ini-syntax files.
0034
0035 Implements all features I know of in .ini files:
0036
0037 * sections with a [ in the first column
0038 * assignment via a=b or a: b
0039 * rfc822-style continuation lines (i.e., start the next line
0040 with indentation to make it a continuation).
0041 * ; or # for comments
0042
0043 This class should be subclassed. Subclasses may only need to
0044 implement the .assignment() method. You may want to use the
0045 .section attribute, which holds the current section (or None if no
0046 section has been defined yet).
0047
0048 Use .parse_error(message) when you encounter a problem; this will
0049 create an exception that will note the filename and line in which
0050 the problem occurs.
0051 """
0052
0053 def __init__(self):
0054 self.reset()
0055
0056 def reset(self):
0057 pass
0058
0059 def load(self, filename, encoding=None):
0060 fileobj = open(filename, 'rb')
0061 self.loadfile(fileobj, filename=filename, encoding=encoding)
0062 fileobj.close()
0063
0064 def loadfile(self, fileobj, filename=None, encoding=None):
0065 self.start_lineno = 0
0066 if filename is None:
0067 filename = getattr(fileobj, 'name', None)
0068 self.filename = filename
0069
0070
0071 self.start_lineno = 0
0072 self.lineno = 0
0073 self.encoding = encoding
0074 def strip_newline(l):
0075 if l.endswith('\n'):
0076 return l[:-1]
0077 else:
0078 return l
0079 self.stream = [strip_newline(l) for l in fileobj.readlines()]
0080 self.process_file()
0081 del self.filename
0082 del self.encoding
0083 del self.lineno
0084
0085 def loadstring(self, string, filename=None):
0086 self.stream = string.splitlines()
0087 self.filename = filename
0088 self.start_lineno = 0
0089 self.lineno = 0
0090 self.encoding = None
0091 self.process_file()
0092 del self.stream
0093 del self.filename
0094 del self.lineno
0095 del self.encoding
0096
0097 def process_file(self):
0098 self.section = None
0099 last_name = None
0100 accumulated_content = None
0101 for line in self.stream:
0102 self.lineno += 1
0103
0104 if self.encoding:
0105 line = line.decode(self.encoding)
0106 end_assignment = False
0107 if not line.strip():
0108 if last_name is not None:
0109 self.process_assignment(
0110 last_name,
0111 accumulated_content)
0112 last_name = accumulated_content = None
0113 self.start_lineno = self.lineno
0114 continue
0115 elif line[0] in (' ', '\t'):
0116 if not last_name:
0117 self.error_continuation_without_assignment(line)
0118 else:
0119 accumulated_content.append(line)
0120 continue
0121 elif self.get_section(line) is not None:
0122 if last_name is not None:
0123 self.process_assignment(
0124 last_name,
0125 accumulated_content)
0126 last_name = accumulated_content = None
0127 self.start_lineno = self.lineno
0128 self.new_section(self.get_section(line))
0129 elif self.get_comment(line) is not None:
0130 if last_name is not None:
0131 self.process_assignment(
0132 last_name,
0133 accumulated_content)
0134 last_name = accumulated_content = None
0135 self.start_lineno = self.lineno
0136 self.add_comment(self.get_comment(line))
0137 else:
0138 if last_name is not None:
0139 self.process_assignment(
0140 last_name,
0141 accumulated_content)
0142 last_name, accumulated_content = self.split_name_value(line)
0143 self.start_lineno = self.lineno
0144 if last_name is not None:
0145 self.process_assignment(
0146 last_name,
0147 accumulated_content)
0148
0149 def split_name_value(self, line):
0150 colon_pos = line.find(':')
0151 equal_pos = line.find('=')
0152 if colon_pos == -1 and equal_pos == -1:
0153 self.error_missing_equal(line)
0154 return None
0155 if (colon_pos == -1
0156 or (equal_pos != -1 and equal_pos < colon_pos)):
0157 pos = equal_pos
0158 else:
0159 pos = colon_pos
0160 return line[:pos], [line[pos+1:]]
0161
0162 def get_comment(self, line):
0163 """
0164 Returns None if not a comment
0165 """
0166 line = line.lstrip()
0167 if line.startswith(';') or line.startswith('#'):
0168 return line[1:]
0169 return None
0170
0171 def get_section(self, line):
0172 """
0173 Returns None if not a section
0174 """
0175 line = line.strip()
0176 if not line.startswith('['):
0177 return None
0178 if not line.endswith(']'):
0179 self.error_section_without_end_bracket(line)
0180 return None
0181 return line[1:-1]
0182
0183 def process_assignment(self, name, accumulated_content):
0184 content = '\n'.join([l.lstrip() for l in accumulated_content])
0185 self.assignment(name.strip(), content)
0186
0187 def assignment(self, name, content):
0188 raise NotImplementedError
0189
0190 def new_section(self, section):
0191 if not section:
0192 self.error_no_section_name()
0193 self.section = section
0194
0195 def add_comment(self, comment):
0196 pass
0197
0198 def error_continuation_without_assignment(self, line):
0199 self.parse_error('Invalid indentation', line)
0200
0201 def error_section_without_end_bracket(self, line):
0202 self.parse_error('Invalid section (must end with ])', line)
0203
0204 def error_missing_equal(self, line):
0205 self.parse_error(
0206 'Lines should look like "name=value" or "name: value"',
0207 line)
0208
0209 def error_no_section_name(self):
0210 self.parse_error(
0211 'Empty section name ([])')
0212
0213 def parse_error(self, msg, line=None, exception=None):
0214 if exception is None:
0215 exception = ParseError
0216 raise exception(
0217 msg,
0218 filename=self.filename,
0219 lineno=self.lineno,
0220 line=line)
0221
0222class BasicParser(INIParser):
0223
0224 """
0225 A simple subclass of INIParser; creates a nested data structure
0226 like ``{'section_name': {'variable': ['values']}}``
0227
0228 Usage::
0229
0230 >>> p = BasicParser()
0231 >>> p.load('config.ini')
0232 >>> data = p.data
0233 """
0234
0235 def reset(self):
0236 self.data = {}
0237 INIParser.reset(self)
0238
0239 def assignment(self, name, content):
0240 if not self.section:
0241 self.parse_error(
0242 'Assignments can only occur inside sections; no section has been defined yet')
0243 section = self.data.setdefault(self.section, {})
0244 section.setdefault(name, []).append(content)