Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add an alternative config parser than the built-in ConfigParser

The built-in one clobbers whitespace, group/option ordering, comments
and generally makes for lousy diffs. This one is intended to preserve
this stuff as much as possible for clean diffs.
  • Loading branch information...
commit 8f6455f595471e76b30d75853016cd0a9728d83a 1 parent 26f53b0
Matthew Brush authored
Showing with 294 additions and 0 deletions.
  1. +149 −0 scripts/geanyscheme/confparse.py
  2. +145 −0 scripts/geanyscheme/confparse.test
149 scripts/geanyscheme/confparse.py
View
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+
+"""
+Replacement for the built-in ``configparser`` module.
+
+See the ``confparse.test`` file for tests and examples.
+"""
+
+class ConfParse(object):
+ """
+ This is a less efficent (probably) and less robust ``ConfigParser`` which
+ only exists because that module/class destroys whitespace and comments.
+ This one is only meant to handle one file at a time and to preserve the
+ original input as much as possible (ie. to make clean diffs).
+ """
+ def __init__(self, filename):
+ self.filename = filename
+ text = open(self.filename, 'r').read()
+ self.lines = [l.strip() for l in text.split('\n')]
+
+ @staticmethod
+ def is_comment(line):
+ line = line.strip()
+ return line.startswith('#') or line.startswith(';')
+
+ @staticmethod
+ def is_group(line):
+ line = line.strip()
+ return line.startswith('[') and line.endswith(']')
+
+ @staticmethod
+ def is_key_value(line):
+ fields = line.split('=')
+ if len(fields) == 1 or not fields[0].strip() or \
+ any(ch in fields[0].strip() for ch in [' ','\t']):
+ return False
+ return True
+
+ def sections(self):
+ sections = []
+ for line in self.lines:
+ if line.startswith('[') and line.endswith(']'):
+ sections.append(line[1:-1])
+ return sections
+
+ def has_section(self, section):
+ return section in self.sections()
+
+ def options(self, section):
+ in_section = False
+ options = []
+ for line in self.lines:
+ if ConfParse.is_comment(line):
+ continue
+ if ConfParse.is_group(line):
+ sect = line[1:-1]
+ in_section = True if sect == section else False
+ continue
+ if in_section and '=' in line:
+ key = line.split('=')[0].strip()
+ options.append(key)
+ return options
+
+ def has_option(self, section, option):
+ return option in self.options(section)
+
+ def get(self, section, option):
+ in_section = False
+ for line in self.lines:
+ if ConfParse.is_comment(line):
+ continue
+ if ConfParse.is_group(line):
+ sect = line[1:-1]
+ in_section = True if sect == section else False
+ continue
+ if in_section and '=' in line:
+ parts = line.split('=')
+ key = parts[0].strip()
+ if key == option:
+ return '='.join(parts[1:]).strip() if len(parts) > 1 else ''
+ return None
+
+ def set(self, section, option, new_value):
+ self.add_section(section)
+ self.add_option(section, option)
+ in_section = False
+ new_lines = self.lines[:]
+ for i, line in enumerate(self.lines):
+ if ConfParse.is_comment(line):
+ continue
+ if ConfParse.is_group(line):
+ sect = line[1:-1]
+ in_section = True if sect == section else False
+ continue
+ if in_section and '=' in line:
+ parts = line.split('=')
+ key = parts[0]
+ value = '='.join(parts[1:])
+ if key == option:
+ new_lines[i] = '%s=%s' % (key, new_value.strip())
+ self.lines = new_lines
+
+ def add_section(self, section):
+ if not self.has_section(section):
+ self.lines.append('[%s]' % section)
+
+ def add_option(self, section, option):
+ self.add_section(section)
+ if not self.has_option(section, option):
+ for i, line in enumerate(self.lines):
+ if ConfParse.is_group(line):
+ group = line[1:-1]
+ if group == section:
+ self.lines.insert(i+1, '%s=' % option)
+ break
+
+ def to_dict(self):
+ out_dict = {}
+ current_group = None
+ for line in self.lines:
+ if ConfParse.is_comment(line):
+ continue
+ if ConfParse.is_group(line):
+ current_group = line[1:-1]
+ out_dict[current_group] = {}
+ continue
+ if current_group is not None and ConfParse.is_key_value(line):
+ fields = line.split('=')
+ if len(fields) > 0:
+ key = fields[0]
+ if len(fields) > 1:
+ value = '='.join(fields[1:])
+ else:
+ value = ''
+ out_dict[current_group][key] = value
+ return out_dict
+
+ def save(self):
+ self.save_as(self.filename)
+
+ def save_as(self, filename):
+ open(filename, 'w').write(str(self))
+
+ def __str__(self):
+ return '\n'.join(self.lines).rstrip('\n') + '\n'
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testfile('confparse.test')
145 scripts/geanyscheme/confparse.test
View
@@ -0,0 +1,145 @@
+Examples and Test file for the ``confparse`` module
+===================================================
+
+The demo file used in this examples/test looks like this:
+
+ [foo]
+ bar=baz
+ mar=maz
+
+ [bloo]
+ blar=blaz
+
+First assume there's a demo config file on disk:
+
+ >>> testconf = '[foo]\nbar=baz\nmar=maz\n\n[bloo]\nblar=blaz\n\n'
+ >>> open('testing___confparse.conf', 'w').write(testconf)
+
+To use the ConfParse class, import it as such:
+
+ >>> from confparse import ConfParse
+
+To create ConfParser and parse a file:
+
+ >>> conf = ConfParse('testing___confparse.conf')
+
+The initialization function must take a filename as an argument, so this
+is wrong:
+
+ >>> conf = ConfParse()
+ Traceback (most recent call last):
+ ...
+ TypeError: __init__() takes exactly 2 arguments (1 given)
+
+To check for the existence of sections and options, do it like this:
+
+ >>> conf.has_section('foo')
+ True
+ >>> conf.has_section('does-not-exist')
+ False
+ >>> conf.has_option('foo', 'bar')
+ True
+ >>> conf.has_option('foo', 'does-not-exist')
+ False
+ >>> conf.has_option('no-such-group', 'bar')
+ False
+
+You can get a list of sections and options in a section like this:
+
+ >>> conf.sections()
+ ['foo', 'bloo']
+ >>> conf.options('foo')
+ ['bar', 'mar']
+
+If the section doesn't exist, calling `options` will return an emtpy list:
+
+ >>> conf.options('does-not-exist')
+ []
+
+You can add an empty section like this:
+
+ >>> conf.add_section('new-section')
+ >>> conf.has_section('new-section')
+ True
+
+You can add a new option like this:
+
+ >>> conf.add_option('new-section', 'new-option')
+ >>> conf.has_option('new-section', 'new-option')
+ True
+
+The new option's value will be an empty string:
+
+ >>> conf.get('new-section', 'new-option')
+ ''
+
+Note that if this the section doesn't exist for ``add_option`` it is added:
+
+ >>> conf.add_option('another-new-section', 'new-option')
+ >>> conf.has_section('another-new-section')
+ True
+
+You can get and set the value of an option like this:
+
+ >>> conf.set('foo', 'mar', 'new-value')
+ >>> conf.get('foo', 'mar')
+ 'new-value'
+
+If you specify a section or option with ``set`` that doesn't exist, they
+will be added, for example:
+
+ >>> conf.has_section('brand-new-section')
+ False
+ >>> conf.set('brand-new-section', 'brand-new-option', 'brand-new-value')
+ >>> conf.has_section('brand-new-section')
+ True
+ >>> conf.get('brand-new-section', 'brand-new-option')
+ 'brand-new-value'
+
+You can get a dictionary of the conf file like this (code mangled for tests):
+
+ >>> open('testing___confparse.conf', 'w').write('[foo]\nbar=baz')
+ >>> conf = ConfParse('testing___confparse.conf')
+ >>> conf.to_dict()
+ {'foo': {'bar': 'baz'}}
+
+You can get the raw text of the conf file using the ``__str__`` method:
+
+ >>> str(conf)
+ '[foo]\nbar=baz\n'
+
+There's also a few static methods used during parsing that are generally
+not very useful externally, but none-the-less (for testing purposes):
+
+ >>> all(ConfParse.is_comment(txt) for txt in ['# foo','; foo',' # foo'])
+ True
+ >>> any(ConfParse.is_comment(txt) for txt in ['foo','[foo]'])
+ False
+ >>> all(ConfParse.is_group(txt) for txt in ['[foo]', ' [foo]'])
+ True
+ >>> any(ConfParse.is_group(txt) for txt in ['foo','[foo','foo]'])
+ False
+ >>> all(ConfParse.is_key_value(txt) for txt in ['key=value', 'key = value', 'key='])
+ True
+ >>> any(ConfParse.is_key_value(txt) for txt in ['key', 'foo bar=baz', '=bar'])
+ False
+
+To re-save the conf file with the same name:
+
+ >>> conf.set('newsect', 'newgroup', 'newval')
+ >>> conf.save()
+ >>> conf = ConfParse('testing___confparse.conf')
+ >>> conf.get('newsect', 'newgroup')
+ 'newval'
+
+To save the conf file with a new file name:
+
+ >>> conf.save_as('testing___confparse2.conf')
+ >>> import os
+ >>> os.path.exists('testing___confparse2.conf')
+ True
+
+And now to clean up some files from the tests/examples:
+
+ >>> os.unlink('testing___confparse.conf')
+ >>> os.unlink('testing___confparse2.conf')
Please sign in to comment.
Something went wrong with that request. Please try again.