summaryrefslogtreecommitdiff
path: root/reconfigure/parsers
diff options
context:
space:
mode:
authorAndrew Shadura <andrew@shadura.me>2015-08-20 15:58:26 +0200
committerAndrew Shadura <andrew@shadura.me>2015-08-20 15:58:26 +0200
commitff1408420159488a106492ccd11dd234967029b6 (patch)
tree473420cee1c5229a427ec4cafead1aa6c0a26800 /reconfigure/parsers
Imported Upstream version 0.1.29
Diffstat (limited to 'reconfigure/parsers')
-rw-r--r--reconfigure/parsers/__init__.py25
-rw-r--r--reconfigure/parsers/base.py18
-rw-r--r--reconfigure/parsers/bind9.py20
-rw-r--r--reconfigure/parsers/crontab.py82
-rw-r--r--reconfigure/parsers/exports.py47
-rw-r--r--reconfigure/parsers/ini.py70
-rw-r--r--reconfigure/parsers/iniparse/__init__.py25
-rw-r--r--reconfigure/parsers/iniparse/compat.py343
-rw-r--r--reconfigure/parsers/iniparse/config.py293
-rw-r--r--reconfigure/parsers/iniparse/ini.py642
-rw-r--r--reconfigure/parsers/iniparse/utils.py47
-rw-r--r--reconfigure/parsers/iptables.py72
-rw-r--r--reconfigure/parsers/jsonparser.py35
-rw-r--r--reconfigure/parsers/nginx.py87
-rw-r--r--reconfigure/parsers/nsd.py50
-rw-r--r--reconfigure/parsers/squid.py53
-rw-r--r--reconfigure/parsers/ssv.py72
17 files changed, 1981 insertions, 0 deletions
diff --git a/reconfigure/parsers/__init__.py b/reconfigure/parsers/__init__.py
new file mode 100644
index 0000000..8de7aeb
--- /dev/null
+++ b/reconfigure/parsers/__init__.py
@@ -0,0 +1,25 @@
+from base import BaseParser
+from bind9 import BIND9Parser
+from exports import ExportsParser
+from ini import IniFileParser
+from iptables import IPTablesParser
+from jsonparser import JsonParser
+from nginx import NginxParser
+from nsd import NSDParser
+from ssv import SSVParser
+from squid import SquidParser
+from crontab import CrontabParser
+
+__all__ = [
+ 'BaseParser',
+ 'BIND9Parser',
+ 'CrontabParser',
+ 'ExportsParser',
+ 'IniFileParser',
+ 'IPTablesParser',
+ 'JsonParser',
+ 'NginxParser',
+ 'NSDParser',
+ 'SSVParser',
+ 'SquidParser',
+]
diff --git a/reconfigure/parsers/base.py b/reconfigure/parsers/base.py
new file mode 100644
index 0000000..321fe1b
--- /dev/null
+++ b/reconfigure/parsers/base.py
@@ -0,0 +1,18 @@
+class BaseParser (object): # pragma: no cover
+ """
+ A base parser class
+ """
+
+ def parse(self, content):
+ """
+ :param content: string config content
+ :returns: a :class:`reconfigure.nodes.Node` tree
+ """
+ return None
+
+ def stringify(self, tree):
+ """
+ :param tree: a :class:`reconfigure.nodes.Node` tree
+ :returns: string config content
+ """
+ return None
diff --git a/reconfigure/parsers/bind9.py b/reconfigure/parsers/bind9.py
new file mode 100644
index 0000000..d5c8e01
--- /dev/null
+++ b/reconfigure/parsers/bind9.py
@@ -0,0 +1,20 @@
+from reconfigure.nodes import *
+from reconfigure.parsers.nginx import NginxParser
+
+
+class BIND9Parser (NginxParser):
+ """
+ A parser for named.conf
+ """
+
+ tokens = [
+ (r"[\w_]+\s*?.*?{", lambda s, t: ('section_start', t)),
+ (r"[\w\d_:]+?.*?;", lambda s, t: ('option', t)),
+ (r"\s", lambda s, t: 'whitespace'),
+ (r"$^", lambda s, t: 'newline'),
+ (r"\#.*?\n", lambda s, t: ('comment', t)),
+ (r"//.*?\n", lambda s, t: ('comment', t)),
+ (r"/\*.*?\*/", lambda s, t: ('comment', t)),
+ (r"\};", lambda s, t: 'section_end'),
+ ]
+ token_section_end = '};'
diff --git a/reconfigure/parsers/crontab.py b/reconfigure/parsers/crontab.py
new file mode 100644
index 0000000..88dadba
--- /dev/null
+++ b/reconfigure/parsers/crontab.py
@@ -0,0 +1,82 @@
+from reconfigure.nodes import RootNode, Node, PropertyNode
+from reconfigure.parsers import BaseParser
+
+
+class CrontabParser(BaseParser):
+
+ def __init__(self, remove_comments=False):
+ self.remove_comments = remove_comments
+
+ def parse(self, content):
+ root = RootNode()
+ lines = [l.strip() for l in content.splitlines() if l]
+ comment = None
+ for line in lines:
+ if line.startswith('#'):
+ comment = '\n'.join([comment, line]) if comment else line[1:]
+ continue
+ elif line.startswith('@'):
+ special, command = line.split(' ', 1)
+ node = Node('special_task', comment=comment)
+ node.append(PropertyNode('special', special))
+ node.append(PropertyNode('command', command))
+
+ else:
+ split_line = line.split(' ', 5)
+ if len(split_line) <= 3 and '=' in line:
+ name, value = [n.strip() for n in line.split('=')]
+ if not name:
+ continue
+ node = Node('env_setting', comment=comment)
+ node.append(PropertyNode('name', name))
+ node.append(PropertyNode('value', value))
+ elif len(split_line) == 6:
+ node = Node('normal_task', comment=comment)
+ node.append(PropertyNode('minute', split_line[0]))
+ node.append(PropertyNode('hour', split_line[1]))
+ node.append(PropertyNode('day_of_month', split_line[2]))
+ node.append(PropertyNode('month', split_line[3]))
+ node.append(PropertyNode('day_of_week', split_line[4]))
+ node.append(PropertyNode('command', split_line[5]))
+ else:
+ continue
+ root.append(node)
+ comment = None
+ root.comment = comment
+ return root
+
+ def stringify(self, tree):
+ result_lines = []
+ stringify_func = {
+ 'special_task': self.stringify_special_task,
+ 'env_setting': self.stringify_env_setting,
+ 'normal_task': self.stringify_normal_task,
+ }
+ for node in tree:
+ if isinstance(node, Node):
+ string_line = stringify_func.get(node.name, lambda x: '')(node)
+ if node.comment:
+ result_lines.append('#' + node.comment)
+ result_lines.append(string_line)
+ return '\n'.join([line for line in result_lines if line])
+
+ def stringify_special_task(self, node):
+ special_node = node.get('special')
+ command_node = node.get('command')
+ if isinstance(special_node, PropertyNode) and isinstance(command_node, PropertyNode):
+ return ' '.join([special_node.value, command_node.value])
+ return ''
+
+ def stringify_env_setting(self, node):
+ name = node.get('name')
+ value = node.get('value')
+ if isinstance(name, PropertyNode) and isinstance(value, PropertyNode):
+ return ' = '.join([name.value, value.value])
+ return ''
+
+ def stringify_normal_task(self, node):
+ if all([isinstance(child, PropertyNode) for child in node.children]):
+ values_list = [str(pr_node.value).strip() for pr_node in node.children if pr_node.value]
+ if len(values_list) == 6:
+ return ' '.join(values_list)
+ return ''
diff --git a/reconfigure/parsers/exports.py b/reconfigure/parsers/exports.py
new file mode 100644
index 0000000..1941f61
--- /dev/null
+++ b/reconfigure/parsers/exports.py
@@ -0,0 +1,47 @@
+from reconfigure.nodes import *
+from reconfigure.parsers import BaseParser
+from reconfigure.parsers.ssv import SSVParser
+
+
+class ExportsParser (BaseParser):
+ """
+ A parser for NFS' /etc/exports
+ """
+
+ def __init__(self, *args, **kwargs):
+ BaseParser.__init__(self, *args, **kwargs)
+ self.inner = SSVParser(continuation='\\')
+
+ def parse(self, content):
+ tree = self.inner.parse(content)
+ root = RootNode()
+ for export in tree:
+ export_node = Node(export[0].get('value').value)
+ export_node.comment = export.comment
+ clients_node = Node('clients')
+ export_node.append(clients_node)
+ root.append(export_node)
+
+ for client in export[1:]:
+ s = client.get('value').value
+ name = s.split('(')[0]
+ options = ''
+ if '(' in s:
+ options = s.split('(', 1)[1].rstrip(')')
+ client_node = Node(name)
+ client_node.set_property('options', options)
+ clients_node.append(client_node)
+ return root
+
+ def stringify(self, tree):
+ root = RootNode()
+ for export in tree:
+ export_node = Node('line', comment=export.comment)
+ export_node.append(Node('token', PropertyNode('value', export.name)))
+ for client in export['clients']:
+ s = client.name
+ if client['options'].value:
+ s += '(%s)' % client['options'].value
+ export_node.append(Node('token', PropertyNode('value', s)))
+ root.append(export_node)
+ return self.inner.stringify(root)
diff --git a/reconfigure/parsers/ini.py b/reconfigure/parsers/ini.py
new file mode 100644
index 0000000..dbd0c7c
--- /dev/null
+++ b/reconfigure/parsers/ini.py
@@ -0,0 +1,70 @@
+from reconfigure.nodes import *
+from reconfigure.parsers import BaseParser
+from iniparse import INIConfig
+from StringIO import StringIO
+
+
+class IniFileParser (BaseParser):
+ """
+ A parser for standard ``.ini`` config files.
+
+ :param sectionless: if ``True``, allows a section-less attributes appear in the beginning of file
+ """
+
+ def __init__(self, sectionless=False, nullsection='__default__'):
+ self.sectionless = sectionless
+ self.nullsection = nullsection
+
+ def _get_comment(self, container):
+ c = container.contents[0].comment
+ return c.strip() if c else None
+
+ def _set_comment(self, container, comment):
+ if comment:
+ container.contents[0].comment = comment
+ container.contents[0].comment_separator = ';'
+
+ def parse(self, content):
+ content = '\n'.join(filter(None, [x.strip() for x in content.splitlines()]))
+ if self.sectionless:
+ content = '[' + self.nullsection + ']\n' + content
+ data = StringIO(content)
+ cp = INIConfig(data, optionxformvalue=lambda x: x)
+
+ root = RootNode()
+ for section in cp:
+ name = section
+ if self.sectionless and section == self.nullsection:
+ name = None
+ section_node = Node(name)
+ section_node.comment = self._get_comment(cp[section]._lines[0])
+ for option in cp[section]:
+ if option in cp[section]._options:
+ node = PropertyNode(option, cp[section][option])
+ node.comment = self._get_comment(cp[section]._options[option])
+ section_node.children.append(node)
+ root.children.append(section_node)
+ return root
+
+ def stringify(self, tree):
+ cp = INIConfig()
+
+ for section in tree.children:
+ if self.sectionless and section.name is None:
+ sectionname = self.nullsection
+ else:
+ sectionname = section.name
+ cp._new_namespace(sectionname)
+ for option in section.children:
+ if not isinstance(option, PropertyNode):
+ raise TypeError('Third level nodes should be PropertyNodes')
+ cp[sectionname][option.name] = option.value
+ if option.comment:
+ self._set_comment(cp[sectionname]._options[option.name], option.comment)
+ if hasattr(cp[sectionname], '_lines'):
+ self._set_comment(cp[sectionname]._lines[0], section.comment)
+
+ data = str(cp) + '\n'
+ if self.sectionless:
+ data = data.replace('[' + self.nullsection + ']\n', '')
+ return data
diff --git a/reconfigure/parsers/iniparse/__init__.py b/reconfigure/parsers/iniparse/__init__.py
new file mode 100644
index 0000000..618bd20
--- /dev/null
+++ b/reconfigure/parsers/iniparse/__init__.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2001, 2002, 2003 Python Software Foundation
+# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu>
+# Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk>
+# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
+
+from ini import INIConfig, change_comment_syntax
+from config import BasicConfig, ConfigNamespace
+from compat import RawConfigParser, ConfigParser, SafeConfigParser
+from utils import tidy
+
+from ConfigParser import DuplicateSectionError, \
+ NoSectionError, NoOptionError, \
+ InterpolationMissingOptionError, \
+ InterpolationDepthError, \
+ InterpolationSyntaxError, \
+ DEFAULTSECT, MAX_INTERPOLATION_DEPTH
+
+__all__ = [
+ 'BasicConfig', 'ConfigNamespace',
+ 'INIConfig', 'tidy', 'change_comment_syntax',
+ 'RawConfigParser', 'ConfigParser', 'SafeConfigParser',
+ 'DuplicateSectionError', 'NoSectionError', 'NoOptionError',
+ 'InterpolationMissingOptionError', 'InterpolationDepthError',
+ 'InterpolationSyntaxError', 'DEFAULTSECT', 'MAX_INTERPOLATION_DEPTH',
+] \ No newline at end of file
diff --git a/reconfigure/parsers/iniparse/compat.py b/reconfigure/parsers/iniparse/compat.py
new file mode 100644
index 0000000..17c4f67
--- /dev/null
+++ b/reconfigure/parsers/iniparse/compat.py
@@ -0,0 +1,343 @@
+# Copyright (c) 2001, 2002, 2003 Python Software Foundation
+# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu>
+# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
+
+"""Compatibility interfaces for ConfigParser
+
+Interfaces of ConfigParser, RawConfigParser and SafeConfigParser
+should be completely identical to the Python standard library
+versions. Tested with the unit tests included with Python-2.3.4
+
+The underlying INIConfig object can be accessed as cfg.data
+"""
+
+import re
+from ConfigParser import DuplicateSectionError, \
+ NoSectionError, NoOptionError, \
+ InterpolationMissingOptionError, \
+ InterpolationDepthError, \
+ InterpolationSyntaxError, \
+ DEFAULTSECT, MAX_INTERPOLATION_DEPTH
+
+# These are imported only for compatiability.
+# The code below does not reference them directly.
+from ConfigParser import Error, InterpolationError, \
+ MissingSectionHeaderError, ParsingError
+
+import ini
+
+class RawConfigParser(object):
+ def __init__(self, defaults=None, dict_type=dict):
+ if dict_type != dict:
+ raise ValueError('Custom dict types not supported')
+ self.data = ini.INIConfig(defaults=defaults, optionxformsource=self)
+
+ def optionxform(self, optionstr):
+ return optionstr.lower()
+
+ def defaults(self):
+ d = {}
+ secobj = self.data._defaults
+ for name in secobj._options:
+ d[name] = secobj._compat_get(name)
+ return d
+
+ def sections(self):
+ """Return a list of section names, excluding [DEFAULT]"""
+ return list(self.data)
+
+ def add_section(self, section):
+ """Create a new section in the configuration.
+
+ Raise DuplicateSectionError if a section by the specified name
+ already exists. Raise ValueError if name is DEFAULT or any of
+ its case-insensitive variants.
+ """
+ # The default section is the only one that gets the case-insensitive
+ # treatment - so it is special-cased here.
+ if section.lower() == "default":
+ raise ValueError, 'Invalid section name: %s' % section
+
+ if self.has_section(section):
+ raise DuplicateSectionError(section)
+ else:
+ self.data._new_namespace(section)
+
+ def has_section(self, section):
+ """Indicate whether the named section is present in the configuration.
+
+ The DEFAULT section is not acknowledged.
+ """
+ return (section in self.data)
+
+ def options(self, section):
+ """Return a list of option names for the given section name."""
+ if section in self.data:
+ return list(self.data[section])
+ else:
+ raise NoSectionError(section)
+
+ def read(self, filenames):
+ """Read and parse a filename or a list of filenames.
+
+ Files that cannot be opened are silently ignored; this is
+ designed so that you can specify a list of potential
+ configuration file locations (e.g. current directory, user's
+ home directory, systemwide directory), and all existing
+ configuration files in the list will be read. A single
+ filename may also be given.
+ """
+ files_read = []
+ if isinstance(filenames, basestring):
+ filenames = [filenames]
+ for filename in filenames:
+ try:
+ fp = open(filename)
+ except IOError:
+ continue
+ files_read.append(filename)
+ self.data._readfp(fp)
+ fp.close()
+ return files_read
+
+ def readfp(self, fp, filename=None):
+ """Like read() but the argument must be a file-like object.
+
+ The `fp' argument must have a `readline' method. Optional
+ second argument is the `filename', which if not given, is
+ taken from fp.name. If fp has no `name' attribute, `<???>' is
+ used.
+ """
+ self.data._readfp(fp)
+
+ def get(self, section, option, vars=None):
+ if not self.has_section(section):
+ raise NoSectionError(section)
+ if vars is not None and option in vars:
+ value = vars[option]
+
+ sec = self.data[section]
+ if option in sec:
+ return sec._compat_get(option)
+ else:
+ raise NoOptionError(option, section)
+
+ def items(self, section):
+ if section in self.data:
+ ans = []
+ for opt in self.data[section]:
+ ans.append((opt, self.get(section, opt)))
+ return ans
+ else:
+ raise NoSectionError(section)
+
+ def getint(self, section, option):
+ return int(self.get(section, option))
+
+ def getfloat(self, section, option):
+ return float(self.get(section, option))
+
+ _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True,
+ '0': False, 'no': False, 'false': False, 'off': False}
+
+ def getboolean(self, section, option):
+ v = self.get(section, option)
+ if v.lower() not in self._boolean_states:
+ raise ValueError, 'Not a boolean: %s' % v
+ return self._boolean_states[v.lower()]
+
+ def has_option(self, section, option):
+ """Check for the existence of a given option in a given section."""
+ if section in self.data:
+ sec = self.data[section]
+ else:
+ raise NoSectionError(section)
+ return (option in sec)
+
+ def set(self, section, option, value):
+ """Set an option."""
+ if section in self.data:
+ self.data[section][option] = value
+ else:
+ raise NoSectionError(section)
+
+ def write(self, fp):
+ """Write an .ini-format representation of the configuration state."""
+ fp.write(str(self.data))
+
+ def remove_option(self, section, option):
+ """Remove an option."""
+ if section in self.data:
+ sec = self.data[section]
+ else:
+ raise NoSectionError(section)
+ if option in sec:
+ del sec[option]
+ return 1
+ else:
+ return 0
+
+ def remove_section(self, section):
+ """Remove a file section."""
+ if not self.has_section(section):
+ return False
+ del self.data[section]
+ return True
+
+
+class ConfigDict(object):
+ """Present a dict interface to a ini section."""
+
+ def __init__(self, cfg, section, vars):
+ self.cfg = cfg
+ self.section = section
+ self.vars = vars
+
+ def __getitem__(self, key):
+ try:
+ return RawConfigParser.get(self.cfg, self.section, key, self.vars)
+ except (NoOptionError, NoSectionError):
+ raise KeyError(key)
+
+
+class ConfigParser(RawConfigParser):
+
+ def get(self, section, option, raw=False, vars=None):
+ """Get an option value for a given section.
+
+ All % interpolations are expanded in the return values, based on the
+ defaults passed into the constructor, unless the optional argument
+ `raw' is true. Additional substitutions may be provided using the
+ `vars' argument, which must be a dictionary whose contents overrides
+ any pre-existing defaults.
+
+ The section DEFAULT is special.
+ """
+ if section != DEFAULTSECT and not self.has_section(section):
+ raise NoSectionError(section)
+
+ option = self.optionxform(option)
+ value = RawConfigParser.get(self, section, option, vars)
+
+ if raw:
+ return value
+ else:
+ d = ConfigDict(self, section, vars)
+ return self._interpolate(section, option, value, d)
+
+ def _interpolate(self, section, option, rawval, vars):
+ # do the string interpolation
+ value = rawval
+ depth = MAX_INTERPOLATION_DEPTH
+ while depth: # Loop through this until it's done
+ depth -= 1
+ if "%(" in value:
+ try:
+ value = value % vars
+ except KeyError, e:
+ raise InterpolationMissingOptionError(
+ option, section, rawval, e.args[0])
+ else:
+ break
+ if value.find("%(") != -1:
+ raise InterpolationDepthError(option, section, rawval)
+ return value
+
+ def items(self, section, raw=False, vars=None):
+ """Return a list of tuples with (name, value) for each option
+ in the section.
+
+ All % interpolations are expanded in the return values, based on the
+ defaults passed into the constructor, unless the optional argument
+ `raw' is true. Additional substitutions may be provided using the
+ `vars' argument, which must be a dictionary whose contents overrides
+ any pre-existing defaults.
+
+ The section DEFAULT is special.
+ """
+ if section != DEFAULTSECT and not self.has_section(section):
+ raise NoSectionError(section)
+ if vars is None:
+ options = list(self.data[section])
+ else:
+ options = []
+ for x in self.data[section]:
+ if x not in vars:
+ options.append(x)
+ options.extend(vars.keys())
+
+ if "__name__" in options:
+ options.remove("__name__")
+
+ d = ConfigDict(self, section, vars)
+ if raw:
+ return [(option, d[option])
+ for option in options]
+ else:
+ return [(option, self._interpolate(section, option, d[option], d))
+ for option in options]
+
+
+class SafeConfigParser(ConfigParser):
+ _interpvar_re = re.compile(r"%\(([^)]+)\)s")
+ _badpercent_re = re.compile(r"%[^%]|%$")
+
+ def set(self, section, option, value):
+ if not isinstance(value, basestring):
+ raise TypeError("option values must be strings")
+ # check for bad percent signs:
+ # first, replace all "good" interpolations
+ tmp_value = self._interpvar_re.sub('', value)
+ # then, check if there's a lone percent sign left
+ m = self._badpercent_re.search(tmp_value)
+ if m:
+ raise ValueError("invalid interpolation syntax in %r at "
+ "position %d" % (value, m.start()))
+
+ ConfigParser.set(self, section, option, value)
+
+ def _interpolate(self, section, option, rawval, vars):
+ # do the string interpolation
+ L = []
+ self._interpolate_some(option, L, rawval, section, vars, 1)
+ return ''.join(L)
+
+ _interpvar_match = re.compile(r"%\(([^)]+)\)s").match
+
+ def _interpolate_some(self, option, accum, rest, section, map, depth):
+ if depth > MAX_INTERPOLATION_DEPTH:
+ raise InterpolationDepthError(option, section, rest)
+ while rest:
+ p = rest.find("%")
+ if p < 0:
+ accum.append(rest)
+ return
+ if p > 0:
+ accum.append(rest[:p])
+ rest = rest[p:]
+ # p is no longer used
+ c = rest[1:2]
+ if c == "%":
+ accum.append("%")
+ rest = rest[2:]
+ elif c == "(":
+ m = self._interpvar_match(rest)
+ if m is None:
+ raise InterpolationSyntaxError(option, section,
+ "bad interpolation variable reference %r" % rest)
+ var = m.group(1)
+ rest = rest[m.end():]
+ try:
+ v = map[var]
+ except KeyError:
+ raise InterpolationMissingOptionError(
+ option, section, rest, var)
+ if "%" in v:
+ self._interpolate_some(option, accum, v,
+ section, map, depth + 1)
+ else:
+ accum.append(v)
+ else:
+ raise InterpolationSyntaxError(
+ option, section,
+ "'%' must be followed by '%' or '(', found: " + repr(rest)) \ No newline at end of file
diff --git a/reconfigure/parsers/iniparse/config.py b/reconfigure/parsers/iniparse/config.py
new file mode 100644
index 0000000..d007f16
--- /dev/null
+++ b/reconfigure/parsers/iniparse/config.py
@@ -0,0 +1,293 @@
+class ConfigNamespace(object):
+ """Abstract class representing the interface of Config objects.
+
+ A ConfigNamespace is a collection of names mapped to values, where
+ the values may be nested namespaces. Values can be accessed via
+ container notation - obj[key] - or via dotted notation - obj.key.
+ Both these access methods are equivalent.
+
+ To minimize name conflicts between namespace keys and class members,
+ the number of class members should be minimized, and the names of
+ all class members should start with an underscore.
+
+ Subclasses must implement the methods for container-like access,
+ and this class will automatically provide dotted access.
+
+ """
+
+ # Methods that must be implemented by subclasses
+
+ def _getitem(self, key):
+ return NotImplementedError(key)
+
+ def __setitem__(self, key, value):
+ raise NotImplementedError(key, value)
+
+ def __delitem__(self, key):
+ raise NotImplementedError(key)
+
+ def __iter__(self):
+ return NotImplementedError()
+
+ def _new_namespace(self, name):
+ raise NotImplementedError(name)
+
+ def __contains__(self, key):
+ try:
+ self._getitem(key)
+ except KeyError:
+ return False
+ return True
+
+ # Machinery for converting dotted access into container access,
+ # and automatically creating new sections/namespaces.
+ #
+ # To distinguish between accesses of class members and namespace
+ # keys, we first call object.__getattribute__(). If that succeeds,
+ # the name is assumed to be a class member. Otherwise it is
+ # treated as a namespace key.
+ #
+ # Therefore, member variables should be defined in the class,
+ # not just in the __init__() function. See BasicNamespace for
+ # an example.
+
+ def __getitem__(self, key):
+ try:
+ return self._getitem(key)
+ except KeyError:
+ return Undefined(key, self)
+
+ def __getattr__(self, name):
+ try:
+ return self._getitem(name)
+ except KeyError:
+ if name.startswith('__') and name.endswith('__'):
+ raise AttributeError
+ return Undefined(name, self)
+
+ def __setattr__(self, name, value):
+ try:
+ object.__getattribute__(self, name)
+ object.__setattr__(self, name, value)
+ except AttributeError:
+ self.__setitem__(name, value)
+
+ def __delattr__(self, name):
+ try:
+ object.__getattribute__(self, name)
+ object.__delattr__(self, name)
+ except AttributeError:
+ self.__delitem__(name)
+
+ # During unpickling, Python checks if the class has a __setstate__
+ # method. But, the data dicts have not been initialised yet, which
+ # leads to _getitem and hence __getattr__ raising an exception. So
+ # we explicitly impement default __setstate__ behavior.
+ def __setstate__(self, state):
+ self.__dict__.update(state)
+
+class Undefined(object):
+ """Helper class used to hold undefined names until assignment.
+
+ This class helps create any undefined subsections when an
+ assignment is made to a nested value. For example, if the
+ statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet.
+ """
+
+ def __init__(self, name, namespace):
+ object.__setattr__(self, 'name', name)
+ object.__setattr__(self, 'namespace', namespace)
+
+ def __setattr__(self, name, value):
+ obj = self.namespace._new_namespace(self.name)
+ obj[name] = value
+
+ def __setitem__(self, name, value):
+ obj = self.namespace._new_namespace(self.name)
+ obj[name] = value
+
+
+# ---- Basic implementation of a ConfigNamespace
+
+class BasicConfig(ConfigNamespace):
+ """Represents a hierarchical collection of named values.
+
+ Values are added using dotted notation:
+
+ >>> n = BasicConfig()
+ >>> n.x = 7
+ >>> n.name.first = 'paramjit'
+ >>> n.name.last = 'oberoi'
+
+ ...and accessed the same way, or with [...]:
+
+ >>> n.x
+ 7
+ >>> n.name.first
+ 'paramjit'
+ >>> n.name.last
+ 'oberoi'
+ >>> n['x']
+ 7
+ >>> n['name']['first']
+ 'paramjit'
+
+ Iterating over the namespace object returns the keys:
+
+ >>> l = list(n)
+ >>> l.sort()
+ >>> l
+ ['name', 'x']
+
+ Values can be deleted using 'del' and printed using 'print'.
+
+ >>> n.aaa = 42
+ >>> del n.x
+ >>> print n
+ aaa = 42
+ name.first = paramjit
+ name.last = oberoi
+
+ Nested namepsaces are also namespaces:
+
+ >>> isinstance(n.name, ConfigNamespace)
+ True
+ >>> print n.name
+ first = paramjit
+ last = oberoi
+ >>> sorted(list(n.name))
+ ['first', 'last']
+
+ Finally, values can be read from a file as follows:
+
+ >>> from StringIO import StringIO
+ >>> sio = StringIO('''
+ ... # comment
+ ... ui.height = 100
+ ... ui.width = 150
+ ... complexity = medium
+ ... have_python
+ ... data.secret.password = goodness=gracious me
+ ... ''')
+ >>> n = BasicConfig()
+ >>> n._readfp(sio)
+ >>> print n
+ complexity = medium
+ data.secret.password = goodness=gracious me
+ have_python
+ ui.height = 100
+ ui.width = 150
+ """
+
+ # this makes sure that __setattr__ knows this is not a namespace key
+ _data = None
+
+ def __init__(self):
+ self._data = {}
+
+ def _getitem(self, key):
+ return self._data[key]
+
+ def __setitem__(self, key, value):
+ self._data[key] = value
+
+ def __delitem__(self, key):
+ del self._data[key]
+
+ def __iter__(self):
+ return iter(self._data)
+
+ def __str__(self, prefix=''):
+ lines = []
+ keys = self._data.keys()
+ keys.sort()
+ for name in keys:
+ value = self._data[name]
+ if isinstance(value, ConfigNamespace):
+ lines.append(value.__str__(prefix='%s%s.' % (prefix,name)))
+ else:
+ if value is None:
+ lines.append('%s%s' % (prefix, name))
+ else:
+ lines.append('%s%s = %s' % (prefix, name, value))
+ return '\n'.join(lines)
+
+ def _new_namespace(self, name):
+ obj = BasicConfig()
+ self._data[name] = obj
+ return obj
+
+ def _readfp(self, fp):
+ while True:
+ line = fp.readline()
+ if not line:
+ break
+
+ line = line.strip()
+ if not line: continue
+ if line[0] == '#': continue
+ data = line.split('=', 1)
+ if len(data) == 1:
+ name = line
+ value = None
+ else:
+ name = data[0].strip()
+ value = data[1].strip()
+ name_components = name.split('.')
+ ns = self
+ for n in name_components[:-1]:
+ if n in ns:
+ ns = ns[n]
+ if not isinstance(ns, ConfigNamespace):
+ raise TypeError('value-namespace conflict', n)
+ else:
+ ns = ns._new_namespace(n)
+ ns[name_components[-1]] = value
+
+
+# ---- Utility functions
+
+def update_config(target, source):
+ """Imports values from source into target.
+
+ Recursively walks the <source> ConfigNamespace and inserts values
+ into the <target> ConfigNamespace. For example:
+
+ >>> n = BasicConfig()
+ >>> n.playlist.expand_playlist = True
+ >>> n.ui.display_clock = True
+ >>> n.ui.display_qlength = True
+ >>> n.ui.width = 150
+ >>> print n
+ playlist.expand_playlist = True
+ ui.display_clock = True
+ ui.display_qlength = True
+ ui.width = 150
+
+ >>> from iniparse import ini
+ >>> i = ini.INIConfig()
+ >>> update_config(i, n)
+ >>> print i
+ [playlist]
+ expand_playlist = True
+ <BLANKLINE>
+ [ui]
+ display_clock = True
+ display_qlength = True
+ width = 150
+
+ """
+ for name in source:
+ value = source[name]
+ if isinstance(value, ConfigNamespace):
+ if name in target:
+ myns = target[name]
+ if not isinstance(myns, ConfigNamespace):
+ raise TypeError('value-namespace conflict')
+ else:
+ myns = target._new_namespace(name)
+ update_config(myns, value)
+ else:
+ target[name] = value
+
+
diff --git a/reconfigure/parsers/iniparse/ini.py b/reconfigure/parsers/iniparse/ini.py
new file mode 100644
index 0000000..7881fd2
--- /dev/null
+++ b/reconfigure/parsers/iniparse/ini.py
@@ -0,0 +1,642 @@
+"""Access and/or modify INI files
+
+* Compatiable with ConfigParser
+* Preserves order of sections & options
+* Preserves comments/blank lines/etc
+* More conveninet access to data
+
+Example:
+
+ >>> from StringIO import StringIO
+ >>> sio = StringIO('''# configure foo-application
+ ... [foo]
+ ... bar1 = qualia
+ ... bar2 = 1977
+ ... [foo-ext]
+ ... special = 1''')
+
+ >>> cfg = INIConfig(sio)
+ >>> print cfg.foo.bar1
+ qualia
+ >>> print cfg['foo-ext'].special
+ 1
+ >>> cfg.foo.newopt = 'hi!'
+ >>> cfg.baz.enabled = 0
+
+ >>> print cfg
+ # configure foo-application
+ [foo]
+ bar1 = qualia
+ bar2 = 1977
+ newopt = hi!
+ [foo-ext]
+ special = 1
+ <BLANKLINE>
+ [baz]
+ enabled = 0
+
+"""
+
+# An ini parser that supports ordered sections/options
+# Also supports updates, while preserving structure
+# Backward-compatiable with ConfigParser
+
+import re
+from ConfigParser import DEFAULTSECT, ParsingError, MissingSectionHeaderError
+
+import config
+
+class LineType(object):
+ line = None
+
+ def __init__(self, line=None):
+ if line is not None:
+ self.line = line.strip('\n')
+
+ # Return the original line for unmodified objects
+ # Otherwise construct using the current attribute values
+ def __str__(self):
+ if self.line is not None:
+ return self.line
+ else:
+ return self.to_string()
+
+ # If an attribute is modified after initialization
+ # set line to None since it is no longer accurate.
+ def __setattr__(self, name, value):
+ if hasattr(self,name):
+ self.__dict__['line'] = None
+ self.__dict__[name] = value
+
+ def to_string(self):
+ raise Exception('This method must be overridden in derived classes')
+
+
+class SectionLine(LineType):
+ regex = re.compile(r'^\['
+ r'(?P<name>[^]]+)'
+ r'\]\s*'
+ r'((?P<csep>;|#)(?P<comment>.*))?$')
+
+ def __init__(self, name, comment=None, comment_separator=None,
+ comment_offset=-1, line=None):
+ super(SectionLine, self).__init__(line)
+ self.name = name
+ self.comment = comment
+ self.comment_separator = comment_separator
+ self.comment_offset = comment_offset
+
+ def to_string(self):
+ out = '[' + self.name + ']'
+ if self.comment is not None:
+ # try to preserve indentation of comments
+ out = (out+' ').ljust(self.comment_offset)
+ out = out + self.comment_separator + self.comment
+ return out
+
+ def parse(cls, line):
+ m = cls.regex.match(line.rstrip())
+ if m is None:
+ return None
+ return cls(m.group('name'), m.group('comment'),
+ m.group('csep'), m.start('csep'),
+ line)
+ parse = classmethod(parse)
+
+
+class OptionLine(LineType):
+ def __init__(self, name, value, separator='=', comment=None,
+ comment_separator=None, comment_offset=-1, line=None):
+ super(OptionLine, self).__init__(line)
+ self.name = name
+ self.value = value
+ self.separator = separator
+ self.comment = comment
+ self.comment_separator = comment_separator
+ self.comment_offset = comment_offset
+
+ def to_string(self):
+ out = '%s%s%s' % (self.name, self.separator, self.value)
+ if self.comment is not None:
+ # try to preserve indentation of comments
+ out = (out+' ').ljust(self.comment_offset)
+ out = out + self.comment_separator + self.comment
+ return out
+
+ regex = re.compile(r'^(?P<name>[^:=\s[][^:=]*)'
+ r'(?P<sep>[:=]\s*)'
+ r'(?P<value>.*)$')
+
+ def parse(cls, line):
+ m = cls.regex.match(line.rstrip())
+ if m is None:
+ return None
+
+ name = m.group('name').rstrip()
+ value = m.group('value')
+ sep = m.group('name')[len(name):] + m.group('sep')
+
+ # comments are not detected in the regex because
+ # ensuring total compatibility with ConfigParser
+ # requires that:
+ # option = value ;comment // value=='value'
+ # option = value;1 ;comment // value=='value;1 ;comment'
+ #
+ # Doing this in a regex would be complicated. I
+ # think this is a bug. The whole issue of how to
+ # include ';' in the value needs to be addressed.
+ # Also, '#' doesn't mark comments in options...
+
+ coff = value.find(';')
+ if coff != -1 and value[coff-1].isspace():
+ comment = value[coff+1:]
+ csep = value[coff]
+ value = value[:coff].rstrip()
+ coff = m.start('value') + coff
+ else:
+ comment = None
+ csep = None
+ coff = -1
+
+ return cls(name, value, sep, comment, csep, coff, line)
+ parse = classmethod(parse)
+
+
+def change_comment_syntax(comment_chars='%;#', allow_rem=False):
+ comment_chars = re.sub(r'([\]\-\^])', r'\\\1', comment_chars)
+ regex = r'^(?P<csep>[%s]' % comment_chars
+ if allow_rem:
+ regex += '|[rR][eE][mM]'
+ regex += r')(?P<comment>.*)$'
+ CommentLine.regex = re.compile(regex)
+
+class CommentLine(LineType):
+ regex = re.compile(r'^(?P<csep>[;#]|[rR][eE][mM])'
+ r'(?P<comment>.*)$')
+
+ def __init__(self, comment='', separator='#', line=None):
+ super(CommentLine, self).__init__(line)
+ self.comment = comment
+ self.separator = separator
+
+ def to_string(self):
+ return self.separator + self.comment
+
+ def parse(cls, line):
+ m = cls.regex.match(line.rstrip())
+ if m is None:
+ return None
+ return cls(m.group('comment'), m.group('csep'), line)
+ parse = classmethod(parse)
+
+
+class EmptyLine(LineType):
+ # could make this a singleton
+ def to_string(self):
+ return ''
+
+ value = property(lambda _: '')
+
+ def parse(cls, line):
+ if line.strip(): return None
+ return cls(line)
+ parse = classmethod(parse)
+
+
+class ContinuationLine(LineType):
+ regex = re.compile(r'^\s+(?P<value>.*)$')
+
+ def __init__(self, value, value_offset=None, line=None):
+ super(ContinuationLine, self).__init__(line)
+ self.value = value
+ if value_offset is None:
+ value_offset = 8
+ self.value_offset = value_offset
+
+ def to_string(self):
+ return ' '*self.value_offset + self.value
+
+ def parse(cls, line):
+ m = cls.regex.match(line.rstrip())
+ if m is None:
+ return None
+ return cls(m.group('value'), m.start('value'), line)
+ parse = classmethod(parse)
+
+
+class LineContainer(object):
+ def __init__(self, d=None):
+ self.contents = []
+ self.orgvalue = None
+ if d:
+ if isinstance(d, list): self.extend(d)
+ else: self.add(d)
+
+ def add(self, x):
+ self.contents.append(x)
+
+ def extend(self, x):
+ for i in x: self.add(i)
+
+ def get_name(self):
+ return self.contents[0].name
+
+ def set_name(self, data):
+ self.contents[0].name = data
+
+ def get_value(self):
+ if self.orgvalue is not None:
+ return self.orgvalue
+ elif len(self.contents) == 1:
+ return self.contents[0].value
+ else:
+ return '\n'.join([('%s' % x.value) for x in self.contents
+ if not isinstance(x, CommentLine)])
+
+ def set_value(self, data):
+ self.orgvalue = data
+ lines = ('%s' % data).split('\n')
+
+ # If there is an existing ContinuationLine, use its offset
+ value_offset = None
+ for v in self.contents:
+ if isinstance(v, ContinuationLine):
+ value_offset = v.value_offset
+ break
+
+ # Rebuild contents list, preserving initial OptionLine
+ self.contents = self.contents[0:1]
+ self.contents[0].value = lines[0]
+ del lines[0]
+ for line in lines:
+ if line.strip():
+ self.add(ContinuationLine(line, value_offset))
+ else:
+ self.add(EmptyLine())
+
+ name = property(get_name, set_name)
+ value = property(get_value, set_value)
+
+ def __str__(self):
+ s = [x.__str__() for x in self.contents]
+ return '\n'.join(s)
+
+ def finditer(self, key):
+ for x in self.contents[::-1]:
+ if hasattr(x, 'name') and x.name==key:
+ yield x
+
+ def find(self, key):
+ for x in self.finditer(key):
+ return x
+ raise KeyError(key)
+
+
+def _make_xform_property(myattrname, srcattrname=None):
+ private_attrname = myattrname + 'value'
+ private_srcname = myattrname + 'source'
+ if srcattrname is None:
+ srcattrname = myattrname
+
+ def getfn(self):
+ srcobj = getattr(self, private_srcname)
+ if srcobj is not None:
+ return getattr(srcobj, srcattrname)
+ else:
+ return getattr(self, private_attrname)
+
+ def setfn(self, value):
+ srcobj = getattr(self, private_srcname)
+ if srcobj is not None:
+ setattr(srcobj, srcattrname, value)
+ else:
+ setattr(self, private_attrname, value)
+
+ return property(getfn, setfn)
+
+
+class INISection(config.ConfigNamespace):
+ _lines = None
+ _options = None
+ _defaults = None
+ _optionxformvalue = None
+ _optionxformsource = None
+ _compat_skip_empty_lines = set()
+ def __init__(self, lineobj, defaults = None,
+ optionxformvalue=None, optionxformsource=None):
+ self._lines = [lineobj]
+ self._defaults = defaults
+ self._optionxformvalue = optionxformvalue
+ self._optionxformsource = optionxformsource
+ self._options = {}
+
+ _optionxform = _make_xform_property('_optionxform')
+
+ def _compat_get(self, key):
+ # identical to __getitem__ except that _compat_XXX
+ # is checked for backward-compatible handling
+ if key == '__name__':
+ return self._lines[-1].name
+ if self._optionxform: key = self._optionxform(key)
+ try:
+ value = self._options[key].value
+ del_empty = key in self._compat_skip_empty_lines
+ except KeyError:
+ if self._defaults and key in self._defaults._options:
+ value = self._defaults._options[key].value
+ del_empty = key in self._defaults._compat_skip_empty_lines
+ else:
+ raise
+ if del_empty:
+ value = re.sub('\n+', '\n', value)
+ return value
+
+ def _getitem(self, key):
+ if key == '__name__':
+ return self._lines[-1].name
+ if self._optionxform: key = self._optionxform(key)
+ try:
+ return self._options[key].value
+ except KeyError:
+ if self._defaults and key in self._defaults._options:
+ return self._defaults._options[key].value
+ else:
+ raise
+
+ def __setitem__(self, key, value):
+ if self._optionxform: xkey = self._optionxform(key)
+ else: xkey = key
+ if xkey in self._compat_skip_empty_lines:
+ self._compat_skip_empty_lines.remove(xkey)
+ if xkey not in self._options:
+ # create a dummy object - value may have multiple lines
+ obj = LineContainer(OptionLine(key, ''))
+ self._lines[-1].add(obj)
+ self._options[xkey] = obj
+ # the set_value() function in LineContainer
+ # automatically handles multi-line values
+ self._options[xkey].value = value
+
+ def __delitem__(self, key):
+ if self._optionxform: key = self._optionxform(key)
+ if key in self._compat_skip_empty_lines:
+ self._compat_skip_empty_lines.remove(key)
+ for l in self._lines:
+ remaining = []
+ for o in l.contents:
+ if isinstance(o, LineContainer):
+ n = o.name
+ if self._optionxform: n = self._optionxform(n)
+ if key != n: remaining.append(o)
+ else:
+ remaining.append(o)
+ l.contents = remaining
+ del self._options[key]
+
+ def __iter__(self):
+ d = set()
+ for l in self._lines:
+ for x in l.contents:
+ if isinstance(x, LineContainer):
+ if self._optionxform:
+ ans = self._optionxform(x.name)
+ else:
+ ans = x.name
+ if ans not in d:
+ yield ans
+ d.add(ans)
+ if self._defaults:
+ for x in self._defaults:
+ if x not in d:
+ yield x
+ d.add(x)
+
+ def _new_namespace(self, name):
+ raise Exception('No sub-sections allowed', name)
+
+
+def make_comment(line):
+ return CommentLine(line.rstrip('\n'))
+
+
+def readline_iterator(f):
+ """iterate over a file by only using the file object's readline method"""
+
+ have_newline = False
+ while True:
+ line = f.readline()
+
+ if not line:
+ if have_newline:
+ yield ""
+ return
+
+ if line.endswith('\n'):
+ have_newline = True
+ else:
+ have_newline = False
+
+ yield line
+
+
+def lower(x):
+ return x.lower()
+
+
+class INIConfig(config.ConfigNamespace):
+ _data = None
+ _sections = None
+ _defaults = None
+ _optionxformvalue = None
+ _optionxformsource = None
+ _sectionxformvalue = None
+ _sectionxformsource = None
+ _parse_exc = None
+ _bom = False
+ def __init__(self, fp=None, defaults=None, parse_exc=True,
+ optionxformvalue=lower, optionxformsource=None,
+ sectionxformvalue=None, sectionxformsource=None):
+ self._data = LineContainer()
+ self._parse_exc = parse_exc
+ self._optionxformvalue = optionxformvalue
+ self._optionxformsource = optionxformsource
+ self._sectionxformvalue = sectionxformvalue
+ self._sectionxformsource = sectionxformsource
+ self._sections = {}
+ if defaults is None: defaults = {}
+ self._defaults = INISection(LineContainer(), optionxformsource=self)
+ for name, value in defaults.iteritems():
+ self._defaults[name] = value
+ if fp is not None:
+ self._readfp(fp)
+
+ _optionxform = _make_xform_property('_optionxform', 'optionxform')
+ _sectionxform = _make_xform_property('_sectionxform', 'optionxform')
+
+ def _getitem(self, key):
+ if key == DEFAULTSECT:
+ return self._defaults
+ if self._sectionxform: key = self._sectionxform(key)
+ return self._sections[key]
+
+ def __setitem__(self, key, value):
+ raise Exception('Values must be inside sections', key, value)
+
+ def __delitem__(self, key):
+ if self._sectionxform: key = self._sectionxform(key)
+ for line in self._sections[key]._lines:
+ self._data.contents.remove(line)
+ del self._sections[key]
+
+ def __iter__(self):
+ d = set()
+ d.add(DEFAULTSECT)
+ for x in self._data.contents:
+ if isinstance(x, LineContainer):
+ if x.name not in d:
+ yield x.name
+ d.add(x.name)
+
+ def _new_namespace(self, name):
+ if self._data.contents:
+ self._data.add(EmptyLine())
+ obj = LineContainer(SectionLine(name))
+ self._data.add(obj)
+ if self._sectionxform: name = self._sectionxform(name)
+ if name in self._sections:
+ ns = self._sections[name]
+ ns._lines.append(obj)
+ else:
+ ns = INISection(obj, defaults=self._defaults,
+ optionxformsource=self)
+ self._sections[name] = ns
+ return ns
+
+ def __str__(self):
+ if self._bom:
+ fmt = u'\ufeff%s'
+ else:
+ fmt = '%s'
+ return fmt % self._data.__str__()
+
+ __unicode__ = __str__
+
+ _line_types = [EmptyLine, CommentLine,
+ SectionLine, OptionLine,
+ ContinuationLine]
+
+ def _parse(self, line):
+ for linetype in self._line_types:
+ lineobj = linetype.parse(line)
+ if lineobj:
+ return lineobj
+ else:
+ # can't parse line
+ return None
+
+ def _readfp(self, fp):
+ cur_section = None
+ cur_option = None
+ cur_section_name = None
+ cur_option_name = None
+ pending_lines = []
+ pending_empty_lines = False
+ try:
+ fname = fp.name
+ except AttributeError:
+ fname = '<???>'
+ linecount = 0
+ exc = None
+ line = None
+
+ for line in readline_iterator(fp):
+ # Check for BOM on first line
+ if linecount == 0 and isinstance(line, unicode):
+ if line[0] == u'\ufeff':
+ line = line[1:]
+ self._bom = True
+
+ lineobj = self._parse(line)
+ linecount += 1
+
+ if not cur_section and not isinstance(lineobj,
+ (CommentLine, EmptyLine, SectionLine)):
+ if self._parse_exc:
+ raise MissingSectionHeaderError(fname, linecount, line)
+ else:
+ lineobj = make_comment(line)
+
+ if lineobj is None:
+ if self._parse_exc:
+ if exc is None: exc = ParsingError(fname)
+ exc.append(linecount, line)
+ lineobj = make_comment(line)
+
+ if isinstance(lineobj, ContinuationLine):
+ if cur_option:
+ if pending_lines:
+ cur_option.extend(pending_lines)
+ pending_lines = []
+ if pending_empty_lines:
+ optobj._compat_skip_empty_lines.add(cur_option_name)
+ pending_empty_lines = False
+ cur_option.add(lineobj)
+ else:
+ # illegal continuation line - convert to comment
+ if self._parse_exc:
+ if exc is None: exc = ParsingError(fname)
+ exc.append(linecount, line)
+ lineobj = make_comment(line)
+
+ if isinstance(lineobj, OptionLine):
+ if pending_lines:
+ cur_section.extend(pending_lines)
+ pending_lines = []
+ pending_empty_lines = False
+ cur_option = LineContainer(lineobj)
+ cur_section.add(cur_option)
+ if self._optionxform:
+ cur_option_name = self._optionxform(cur_option.name)
+ else:
+ cur_option_name = cur_option.name
+ if cur_section_name == DEFAULTSECT:
+ optobj = self._defaults
+ else:
+ optobj = self._sections[cur_section_name]
+ optobj._options[cur_option_name] = cur_option
+
+ if isinstance(lineobj, SectionLine):
+ self._data.extend(pending_lines)
+ pending_lines = []
+ pending_empty_lines = False
+ cur_section = LineContainer(lineobj)
+ self._data.add(cur_section)
+ cur_option = None
+ cur_option_name = None
+ if cur_section.name == DEFAULTSECT:
+ self._defaults._lines.append(cur_section)
+ cur_section_name = DEFAULTSECT
+ else:
+ if self._sectionxform:
+ cur_section_name = self._sectionxform(cur_section.name)
+ else:
+ cur_section_name = cur_section.name
+ if cur_section_name not in self._sections:
+ self._sections[cur_section_name] = \
+ INISection(cur_section, defaults=self._defaults,
+ optionxformsource=self)
+ else:
+ self._sections[cur_section_name]._lines.append(cur_section)
+
+ if isinstance(lineobj, (CommentLine, EmptyLine)):
+ pending_lines.append(lineobj)
+ if isinstance(lineobj, EmptyLine):
+ pending_empty_lines = True
+
+ self._data.extend(pending_lines)
+ if line and line[-1]=='\n':
+ self._data.add(EmptyLine())
+
+ if exc:
+ raise exc
+
diff --git a/reconfigure/parsers/iniparse/utils.py b/reconfigure/parsers/iniparse/utils.py
new file mode 100644
index 0000000..9cb7488
--- /dev/null
+++ b/reconfigure/parsers/iniparse/utils.py
@@ -0,0 +1,47 @@
+import compat
+from ini import LineContainer, EmptyLine
+
+def tidy(cfg):
+ """Clean up blank lines.
+
+ This functions makes the configuration look clean and
+ handwritten - consecutive empty lines and empty lines at
+ the start of the file are removed, and one is guaranteed
+ to be at the end of the file.
+ """
+
+ if isinstance(cfg, compat.RawConfigParser):
+ cfg = cfg.data
+ cont = cfg._data.contents
+ i = 1
+ while i < len(cont):
+ if isinstance(cont[i], LineContainer):
+ tidy_section(cont[i])
+ i += 1
+ elif (isinstance(cont[i-1], EmptyLine) and
+ isinstance(cont[i], EmptyLine)):
+ del cont[i]
+ else:
+ i += 1
+
+ # Remove empty first line
+ if cont and isinstance(cont[0], EmptyLine):
+ del cont[0]
+
+ # Ensure a last line
+ if cont and not isinstance(cont[-1], EmptyLine):
+ cont.append(EmptyLine())
+
+def tidy_section(lc):
+ cont = lc.contents
+ i = 1
+ while i < len(cont):
+ if (isinstance(cont[i-1], EmptyLine) and
+ isinstance(cont[i], EmptyLine)):
+ del cont[i]
+ else:
+ i += 1
+
+ # Remove empty first line
+ if len(cont) > 1 and isinstance(cont[1], EmptyLine):
+ del cont[1] \ No newline at end of file
diff --git a/reconfigure/parsers/iptables.py b/reconfigure/parsers/iptables.py
new file mode 100644
index 0000000..46edf60
--- /dev/null
+++ b/reconfigure/parsers/iptables.py
@@ -0,0 +1,72 @@
+from reconfigure.nodes import *
+from reconfigure.parsers import BaseParser
+
+
+class IPTablesParser (BaseParser):
+ """
+ A parser for ``iptables`` configuration as produced by ``iptables-save``
+ """
+
+ def parse(self, content):
+ content = filter(None, [x.strip() for x in content.splitlines() if not x.startswith('#')])
+ root = RootNode()
+ cur_table = None
+ chains = {}
+ for l in content:
+ if l.startswith('*'):
+ cur_table = Node(l[1:])
+ chains = {}
+ root.append(cur_table)
+ elif l.startswith(':'):
+ name = l[1:].split()[0]
+ node = Node(name)
+ node.set_property('default', l.split()[1])
+ chains[name] = node
+ cur_table.append(node)
+ else:
+ comment = None
+ if '#' in l:
+ l, comment = l.split('#')
+ comment = comment.strip()
+ tokens = l.split()
+ if tokens[0] == '-A':
+ tokens.pop(0)
+ node = Node('append')
+ node.comment = comment
+ chain = tokens.pop(0)
+ chains[chain].append(node)
+ while tokens:
+ token = tokens.pop(0)
+ option = Node('option')
+ option.set_property('negative', token == '!')
+ if token == '!':
+ token = tokens.pop(0)
+ option.set_property('name', token.strip('-'))
+ while tokens and not tokens[0].startswith('-') and tokens[0] != '!':
+ option.append(Node('argument', PropertyNode('value', tokens.pop(0))))
+ node.append(option)
+
+ return root
+
+ def stringify(self, tree):
+ data = ''
+ for table in tree.children:
+ data += '*%s\n' % table.name
+ for chain in table.children:
+ data += ':%s %s [0:0]\n' % (chain.name, chain.get('default').value)
+ for chain in table.children:
+ for item in chain.children:
+ if item.name == 'append':
+ data += '-A %s %s%s\n' % (
+ chain.name,
+ ' '.join(
+ ('! ' if o.get('negative').value else '') +
+ ('--' if len(o.get('name').value) > 1 else '-') + o.get('name').value + ' ' +
+ ' '.join(a.get('value').value for a in o.children if a.name == 'argument')
+ for o in item.children
+ if o.name == 'option'
+ ),
+ ' # %s' % item.comment if item.comment else ''
+ )
+ data += 'COMMIT\n'
+ return data
diff --git a/reconfigure/parsers/jsonparser.py b/reconfigure/parsers/jsonparser.py
new file mode 100644
index 0000000..c1eaee6
--- /dev/null
+++ b/reconfigure/parsers/jsonparser.py
@@ -0,0 +1,35 @@
+from reconfigure.nodes import *
+from reconfigure.parsers import BaseParser
+import json
+
+
+class JsonParser (BaseParser):
+ """
+ A parser for JSON files (using ``json`` module)
+ """
+
+ def parse(self, content):
+ node = RootNode()
+ self.load_node_rec(node, json.loads(content))
+ return node
+
+ def load_node_rec(self, node, json):
+ for k, v in json.iteritems():
+ if isinstance(v, dict):
+ child = Node(k)
+ node.children.append(child)
+ self.load_node_rec(child, v)
+ else:
+ node.children.append(PropertyNode(k, v))
+
+ def stringify(self, tree):
+ return json.dumps(self.save_node_rec(tree), indent=4)
+
+ def save_node_rec(self, node):
+ r = {}
+ for child in node.children:
+ if isinstance(child, PropertyNode):
+ r[child.name] = child.value
+ else:
+ r[child.name] = self.save_node_rec(child)
+ return r
diff --git a/reconfigure/parsers/nginx.py b/reconfigure/parsers/nginx.py
new file mode 100644
index 0000000..661cfcd
--- /dev/null
+++ b/reconfigure/parsers/nginx.py
@@ -0,0 +1,87 @@
+from reconfigure.nodes import *
+from reconfigure.parsers import BaseParser
+import re
+
+
+class NginxParser (BaseParser):
+ """
+ A parser for nginx configs
+ """
+
+ tokens = [
+ (r"[\w_]+?.+?;", lambda s, t: ('option', t)),
+ (r"\s", lambda s, t: 'whitespace'),
+ (r"$^", lambda s, t: 'newline'),
+ (r"\#.*?\n", lambda s, t: ('comment', t)),
+ (r"[\w_]+\s*?.*?{", lambda s, t: ('section_start', t)),
+ (r"\}", lambda s, t: 'section_end'),
+ ]
+ token_comment = '#'
+ token_section_end = '}'
+
+ def parse(self, content):
+ scanner = re.Scanner(self.tokens)
+ tokens, remainder = scanner.scan(' '.join(filter(None, content.split(' '))))
+ if remainder:
+ raise Exception('Invalid tokens: %s' % remainder)
+
+ node = RootNode()
+ node.parameter = None
+ node_stack = []
+ next_comment = None
+
+ while len(tokens) > 0:
+ token = tokens[0]
+ tokens = tokens[1:]
+ if token in ['whitespace', 'newline']:
+ continue
+ if token == 'section_end':
+ node = node_stack.pop()
+ if token[0] == 'comment':
+ if not next_comment:
+ next_comment = ''
+ else:
+ next_comment += '\n'
+ next_comment += token[1].strip('#/*').strip()
+ if token[0] == 'option':
+ if ' ' in token[1]:
+ k, v = token[1].split(None, 1)
+ else:
+ v = token[1]
+ k = ''
+ prop = PropertyNode(k.strip(), v[:-1].strip())
+ prop.comment = next_comment
+ next_comment = None
+ node.children.append(prop)
+ if token[0] == 'section_start':
+ line = token[1][:-1].strip().split(None, 1) + [None]
+ section = Node(line[0])
+ section.parameter = line[1]
+ section.comment = next_comment
+ next_comment = None
+ node_stack += [node]
+ node.children.append(section)
+ node = section
+
+ return node
+
+ def stringify(self, tree):
+ return ''.join(self.stringify_rec(node) for node in tree.children)
+
+ def stringify_rec(self, node):
+ if isinstance(node, PropertyNode):
+ if node.name:
+ s = '%s %s;\n' % (node.name, node.value)
+ else:
+ s = '%s;\n' % (node.value)
+ elif isinstance(node, IncludeNode):
+ s = 'include %s;\n' % (node.files)
+ else:
+ result = '\n%s %s {\n' % (node.name, node.parameter or '')
+ for child in node.children:
+ result += '\n'.join('\t' + x for x in self.stringify_rec(child).splitlines()) + '\n'
+ result += self.token_section_end + '\n'
+ s = result
+ if node.comment:
+ s = ''.join(self.token_comment + ' %s\n' % l for l in node.comment.splitlines()) + s
+ return s
diff --git a/reconfigure/parsers/nsd.py b/reconfigure/parsers/nsd.py
new file mode 100644
index 0000000..a1c0522
--- /dev/null
+++ b/reconfigure/parsers/nsd.py
@@ -0,0 +1,50 @@
+from reconfigure.nodes import *
+from reconfigure.parsers import BaseParser
+
+
+class NSDParser (BaseParser):
+ """
+ A parser for NSD DNS server nsd.conf file
+ """
+
+ def parse(self, content):
+ lines = content.splitlines()
+ root = RootNode()
+ last_comment = None
+ node = root
+ for line in lines:
+ line = line.strip()
+ if line:
+ if line.startswith('#'):
+ c = line.strip('#').strip()
+ if last_comment:
+ last_comment += '\n' + c
+ else:
+ last_comment = c
+ continue
+
+ key, value = line.split(':')
+ value = value.strip()
+ key = key.strip()
+ if key in ['server', 'zone', 'key']:
+ node = Node(key, comment=last_comment)
+ root.append(node)
+ else:
+ node.append(PropertyNode(key, value, comment=last_comment))
+ last_comment = None
+ return root
+
+ def stringify_comment(self, line, comment):
+ if comment:
+ return ''.join('# %s\n' % x for x in comment.splitlines()) + line
+ return line
+
+ def stringify(self, tree):
+ r = ''
+ for node in tree.children:
+ r += self.stringify_comment(node.name + ':', node.comment) + '\n'
+ for subnode in node.children:
+ l = '%s: %s' % (subnode.name, subnode.value)
+ r += self.stringify_comment(l, subnode.comment) + '\n'
+ r += '\n'
+ return r
diff --git a/reconfigure/parsers/squid.py b/reconfigure/parsers/squid.py
new file mode 100644
index 0000000..b9b4f97
--- /dev/null
+++ b/reconfigure/parsers/squid.py
@@ -0,0 +1,53 @@
+from reconfigure.nodes import *
+from reconfigure.parsers import BaseParser
+
+
+class SquidParser (BaseParser):
+ """
+ A parser for Squid configs
+ """
+
+ def parse(self, content):
+ lines = filter(None, [x.strip() for x in content.splitlines()])
+ root = RootNode()
+ last_comment = None
+ for line in lines:
+ line = line.strip()
+ if line.startswith('#'):
+ c = line.strip('#').strip()
+ if last_comment:
+ last_comment += '\n' + c
+ else:
+ last_comment = c
+ continue
+ if len(line) == 0:
+ continue
+ tokens = line.split()
+ node = Node('line', Node('arguments'))
+ if last_comment:
+ node.comment = last_comment
+ last_comment = None
+
+ index = 0
+ for token in tokens:
+ if token.startswith('#'):
+ node.comment = ' '.join(tokens[tokens.index(token):])[1:].strip()
+ break
+ if index == 0:
+ node.set_property('name', token)
+ else:
+ node.get('arguments').set_property(str(index), token)
+ index += 1
+ root.append(node)
+ return root
+
+ def stringify(self, tree):
+ r = ''
+ for node in tree.children:
+ if node.comment and '\n' in node.comment:
+ r += ''.join('%s %s\n' % ('#', x) for x in node.comment.splitlines())
+ r += node.get('name').value + ' ' + ' '.join(x.value for x in node.get('arguments').children)
+ if node.comment and not '\n' in node.comment:
+ r += ' # %s' % node.comment
+ r += '\n'
+ return r
diff --git a/reconfigure/parsers/ssv.py b/reconfigure/parsers/ssv.py
new file mode 100644
index 0000000..2119a4a
--- /dev/null
+++ b/reconfigure/parsers/ssv.py
@@ -0,0 +1,72 @@
+from reconfigure.nodes import *
+from reconfigure.parsers import BaseParser
+
+
+class SSVParser (BaseParser):
+ """
+ A parser for files containing space-separated value (notably, ``/etc/fstab`` and friends)
+
+ :param separator: separator character, defaults to whitespace
+ :param maxsplit: max number of tokens per line, defaults to infinity
+ :param comment: character denoting comments
+ :param continuation: line continuation character, None to disable
+ """
+
+ def __init__(self, separator=None, maxsplit=-1, comment='#', continuation=None, *args, **kwargs):
+ self.separator = separator
+ self.maxsplit = maxsplit
+ self.comment = comment
+ self.continuation = continuation
+ BaseParser.__init__(self, *args, **kwargs)
+
+ def parse(self, content):
+ rawlines = content.splitlines()
+ lines = []
+ while rawlines:
+ l = rawlines.pop(0).strip()
+ while self.continuation and rawlines and l.endswith(self.continuation):
+ l = l[:-len(self.continuation)]
+ l += rawlines.pop(0)
+ lines.append(l)
+ root = RootNode()
+ last_comment = None
+ for line in lines:
+ line = line.strip()
+ if line:
+ if line.startswith(self.comment):
+ c = line.strip(self.comment).strip()
+ if last_comment:
+ last_comment += '\n' + c
+ else:
+ last_comment = c
+ continue
+ if len(line) == 0:
+ continue
+ tokens = line.split(self.separator, self.maxsplit)
+ node = Node('line')
+ if last_comment:
+ node.comment = last_comment
+ last_comment = None
+ for token in tokens:
+ if token.startswith(self.comment):
+ node.comment = ' '.join(tokens[tokens.index(token):])[1:].strip()
+ break
+ node.append(Node(
+ name='token',
+ children=[
+ PropertyNode(name='value', value=token)
+ ]
+ ))
+ root.append(node)
+ return root
+
+ def stringify(self, tree):
+ r = ''
+ for node in tree.children:
+ if node.comment and '\n' in node.comment:
+ r += ''.join('%s %s\n' % (self.comment, x) for x in node.comment.splitlines())
+ r += (self.separator or '\t').join(x.get('value').value for x in node.children)
+ if node.comment and not '\n' in node.comment:
+ r += ' # %s' % node.comment
+ r += '\n'
+ return r