← Back to team overview

configglue team mailing list archive

[Merge] lp:~configglue/configglue/schemaconfig-integration-fixes into lp:configglue

 

Ricardo Kirkner has proposed merging lp:~configglue/configglue/schemaconfig-integration-fixes into lp:configglue.

Requested reviews:
  Configglue developers (configglue)


fixes most unit tests (the unicode test is left failing for further discussion, as it seems to fail on lucid but work on maverick)
-- 
https://code.launchpad.net/~configglue/configglue/schemaconfig-integration-fixes/+merge/31790
Your team Configglue developers is requested to review the proposed merge of lp:~configglue/configglue/schemaconfig-integration-fixes into lp:configglue.
=== modified file 'LICENSE'
--- LICENSE	2009-06-15 18:23:08 +0000
+++ LICENSE	2010-08-04 21:21:41 +0000
@@ -1,26 +1,29 @@
-Copyright (c) 2009 Canonical Ltd.
-All rights reserved.
+Copyright 2009, 2010 Canonical Ltd. All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-1. Redistributions of source code must retain the above copyright
-   notice, this list of conditions and the following disclaimer.
-2. Redistributions in binary form must reproduce the above copyright
-   notice, this list of conditions and the following disclaimer in the
-   documentation and/or other materials provided with the distribution.
-3. Neither the name of the University nor the names of its contributors
-   may be used to endorse or promote products derived from this software
-   without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGE.
+modification, are permitted provided that the following conditions are
+met:
+
+   1. Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+   2. Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY CANONICAL LTD. ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL CANONICAL LTD. OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation
+are those of the authors and should not be interpreted as representing
+official policies, either expressed or implied, of Canonical Ltd.

=== added file 'configglue/__init__.py'
--- configglue/__init__.py	1970-01-01 00:00:00 +0000
+++ configglue/__init__.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,17 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+

=== added directory 'configglue/inischema'
=== renamed file 'configglue/__init__.py' => 'configglue/inischema/__init__.py'
--- configglue/__init__.py	2009-03-19 20:01:36 +0000
+++ configglue/inischema/__init__.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 """configglue -- glue for your apps' configuration
 

=== renamed file 'configglue/attributed.py' => 'configglue/inischema/attributed.py'
--- configglue/attributed.py	2009-03-19 20:01:36 +0000
+++ configglue/inischema/attributed.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 """
 AttributtedConfigParser lives here.

=== renamed file 'configglue/glue.py' => 'configglue/inischema/glue.py'
--- configglue/glue.py	2009-03-19 20:01:36 +0000
+++ configglue/inischema/glue.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 """configglue lives here
 """
@@ -20,7 +32,7 @@
         return parser.optionxform(section +'-'+ option)
     
 
-def configglue(fileobj, *filenames, **kwargs):
+def _configglue(fileobj, *filenames, **kwargs):
     """Populate an OptionParser with options and defaults taken from a
     series of files.
 
@@ -76,3 +88,8 @@
                 setattr(options, optname, optval.parser(value))
 
     return op, options, args
+
+from configglue.pyschema import schemaconfigglue, ini2schema
+def configglue(fileobj, *filenames, **kwargs):
+    args = kwargs.pop('args', None)
+    return schemaconfigglue(ini2schema(fileobj), argv=args)

=== renamed file 'configglue/parsers.py' => 'configglue/inischema/parsers.py'
--- configglue/parsers.py	2009-03-19 20:01:36 +0000
+++ configglue/inischema/parsers.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 """Parsers used by TypedConfigParser live here
 """

=== renamed file 'configglue/typed.py' => 'configglue/inischema/typed.py'
--- configglue/typed.py	2009-11-19 19:59:28 +0000
+++ configglue/inischema/typed.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 """ TypedConfigParser lives here """
 from __future__ import absolute_import

=== added directory 'configglue/pyschema'
=== added file 'configglue/pyschema/__init__.py'
--- configglue/pyschema/__init__.py	1970-01-01 00:00:00 +0000
+++ configglue/pyschema/__init__.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,256 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+import __builtin__
+from optparse import OptionParser
+import sys
+
+from configglue.inischema import AttributedConfigParser, parsers
+
+def ini2schema(fd):
+    """
+    Turn a fd that refers to a INI-style schema definition into a
+    SchemaConfigParser object
+    """
+    p = AttributedConfigParser()
+    p.readfp(fd)
+    p.parse_all()
+
+    parser2option = {'unicode': options.StringConfigOption,
+                     'int': options.IntConfigOption,
+                     'bool': options.BoolConfigOption,
+                     'lines': options.LinesConfigOption}
+
+    class MySchema(Schema):
+        pass
+
+    for section_name in p.sections():
+        if section_name == '__main__':
+            section = MySchema
+        else:
+            section = ConfigSection()
+            setattr(MySchema, section_name, section)
+        for option_name in p.options(section_name):
+            option = p.get(section_name, option_name)
+
+            parser = option.attrs.pop('parser', 'unicode')
+            parser_args = option.attrs.pop('parser.args', '').split()
+            parser_fun = getattr(parsers, parser, None)
+            if parser_fun is None:
+                parser_fun = getattr(__builtin__, parser, None)
+            if parser_fun is None:
+                parser_fun = lambda x: x
+
+            attrs = {}
+            option_help = option.attrs.pop('help', None)
+            if option_help is not None:
+                attrs['help'] = option_help
+            if not option.is_empty:
+                attrs['default'] = parser_fun(option.value, *parser_args)
+            option_action = option.attrs.pop('action', None)
+            if option_action is not None:
+                attrs['action'] = option_action
+
+            klass = parser2option.get(parser, options.StringConfigOption)
+            if parser == 'lines':
+                instance = klass(options.StringConfigOption(), **attrs)
+            else:
+                instance = klass(**attrs)
+            setattr(section, option_name, instance)
+
+    return SchemaConfigParser(MySchema())
+
+
+def schemaconfigglue(parser, op=None, argv=None):
+    """Populate an OptionParser with options and defaults taken from a
+    fully loaded SchemaConfigParser.
+    """
+
+    def long_name(option):
+        if option.section.name == '__main__':
+            return option.name
+        return option.section.name + '_' + option.name
+
+    def opt_name(option):
+        return long_name(option).replace('-', '_')
+
+    if op is None:
+        op = OptionParser()
+    if argv is None:
+        argv = sys.argv[1:]
+    schema = parser.schema
+
+    for section in schema.sections():
+        if section.name == '__main__':
+            og = op
+        else:
+            og = op.add_option_group(section.name)
+        for option in section.options():
+            kwargs = {}
+            if option.help:
+                kwargs['help'] = option.help
+            kwargs['default'] = parser.get(section.name, option.name)
+            kwargs['action'] = option.action
+            og.add_option('--' + long_name(option), **kwargs)
+    options, args = op.parse_args(argv)
+
+    for section in schema.sections():
+        for option in section.options():
+            value = getattr(options, opt_name(option))
+            if parser.get(section.name, option.name) != value:
+                # the value has been overridden by an argument;
+                # update it.
+                parser.set(section.name, option.name, value)
+
+    return op, options, args
+
+def super_vars(obj):
+    """An extended version of vars() that walks all base classes."""
+    items = {}
+    if hasattr(obj, '__mro__'):
+        bases = map(vars, obj.__mro__)
+        map(items.update, bases)
+    else:
+        items = vars(obj)
+    return items
+
+NO_DEFAULT = object()
+
+
+class ConfigOption(object):
+    """Base class for Config Options.
+
+    ConfigOptions are never bound to a particular conguration file, and
+    simply describe one particular available option.
+
+    They also know how to parse() the content of a config file in to the right
+    type of object.
+
+    If self.raw == True, then variable interpolation will not be carried out
+    for this config option.
+
+    If self.require_parser == True, then the parse() method will have a second
+    argument, parser, that should receive the whole SchemaConfigParser to
+    do the parsing.  This is needed for config options that need to look at
+    other parts of the config file to be able to carry out their parsing,
+    like DictConfigOptions.
+
+    If self.fatal == True, SchemaConfigParser's parse_all will raise an
+    exception if no value for this option is provided in the configuration
+    file.  Otherwise, the self.default value will be used if the option is
+    omitted.
+
+    In runtime, after instantiating the Schema, each ConfigOption will also
+    know its own name and to which section it belongs.
+    """
+
+    require_parser = False
+
+    def __init__(self, name='', raw=False, default=NO_DEFAULT, fatal=False, help='',
+                 section=None, action='store'):
+        self.name = name
+        self.raw = raw
+        self.fatal = fatal
+        if default is NO_DEFAULT:
+            default = self._get_default()
+        self.default = default
+        self.help = help
+        self.section = section
+        self.action = action
+
+    def __eq__(self, other):
+        try:
+            equal = (self.name == other.name and
+                     self.raw == other.raw and
+                     self.fatal == other.fatal and
+                     self.default == other.default and
+                     self.help == other.help)
+            if self.section is not None and other.section is not None:
+                # only test for section name to avoid recursion
+                equal &= self.section.name == other.section.name
+            else:
+                equal &= (self.section is None and other.section is None)
+        except AttributeError:
+            equal = False
+
+        return equal
+
+    def __repr__(self):
+        extra = ' raw' if self.raw else ''
+        extra += ' fatal' if self.fatal else ''
+        section = self.section.name if self.section is not None else None
+        if section is not None:
+            name = " %s.%s" % (section, self.name)
+        elif self.name:
+            name = " %s" % self.name
+        else:
+            name = ''
+        value = "<ConfigOption%s%s>" % (name, extra)
+        return value
+
+    def _get_default(self):
+        return None
+
+    def parse(self, value):
+        raise NotImplementedError()
+
+
+class ConfigSection(object):
+    """A group of options.
+
+    This class is just a bag you can dump ConfigOptions in.
+
+    After instantiating the Schema, each ConfigSection will know its own
+    name.
+    """
+    def __init__(self, name=''):
+        self.name = name
+
+    def __eq__(self, other):
+        return (self.name == other.name and
+                self.options() == other.options())
+
+    def __repr__(self):
+        if self.name:
+            name = " %s" % self.name
+        else:
+            name = ''
+        value = "<ConfigSection%s>" % name
+        return value
+
+    def has_option(self, name):
+        """Return True if a ConfigOption with the given name is available"""
+        opt = getattr(self, name, None)
+        return isinstance(opt, ConfigOption)
+
+    def option(self, name):
+        """Return a ConfigOption by name"""
+        assert hasattr(self, name), "Invalid ConfigOption name '%s'" % name
+        return getattr(self, name)
+
+    def options(self):
+        """Return a list of all available ConfigOptions within this section"""
+        return [getattr(self, att) for att in vars(self)
+                if isinstance(getattr(self, att), ConfigOption)]
+
+
+# usability tweak -- put everything in the base namespace to make import lines
+# shorter
+from options import (BoolConfigOption, DictConfigOption, IntConfigOption,
+    LinesConfigOption, StringConfigOption, TupleConfigOption)
+from parser import SchemaConfigParser
+from schema import Schema

=== added file 'configglue/pyschema/options.py'
--- configglue/pyschema/options.py	1970-01-01 00:00:00 +0000
+++ configglue/pyschema/options.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,245 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+from configglue.pyschema import ConfigOption, NO_DEFAULT
+
+
+class BoolConfigOption(ConfigOption):
+    """A ConfigOption that is parsed into a bool"""
+
+    def _get_default(self):
+        return False
+
+    def parse(self, value, raw=False):
+        if raw:
+            return value
+
+        if value.lower() in ['y', '1', 'yes', 'on', 'true']:
+            return True
+        elif value.lower() in ['n', '0', 'no', 'off', 'false']:
+            return False
+        else:
+            raise ValueError("Unable to determine boolosity of %r" % value)
+
+
+class IntConfigOption(ConfigOption):
+    """A ConfigOption that is parsed into an int"""
+
+    def _get_default(self):
+        return 0
+
+    def parse(self, value, raw=False):
+        if raw:
+            return value
+
+        return int(value)
+
+
+class LinesConfigOption(ConfigOption):
+    """A ConfigOption that is parsed into a list of objects
+
+    All items in the list need to be of the same type.  The 'item' constructor
+    argument determines the type of the list items. item should be another
+    child of ConfigOption.
+
+    self.require_parser will be True if the item provided in turn has
+    require_parser == True.
+
+    if remove_duplicates == True, duplicate elements in the lines will be
+    removed.  Only the first occurrence of any item will be kept,
+    otherwise the general order of the list will be preserved.
+    """
+
+    def _get_default(self):
+        return []
+
+    def parse(self, value, parser=None, raw=False):
+        def _parse_item(value):
+            if self.require_parser:
+                value = self.item.parse(value, parser=parser, raw=raw)
+            else:
+                value = self.item.parse(value, raw=raw)
+            return value
+        items = [_parse_item(x) for x in value.split('\n') if len(x)]
+        if self.remove_duplicates:
+            filtered_items = []
+            for item in items:
+                if not item in filtered_items:
+                    filtered_items.append(item)
+            items = filtered_items
+        return items
+
+    def __init__(self, item, raw=False, default=NO_DEFAULT, fatal=False,
+        help='', action='store', remove_duplicates=False):
+        super(LinesConfigOption, self).__init__(raw=raw, default=default,
+            fatal=fatal, help=help, action=action)
+        self.item = item
+        self.require_parser = item.require_parser
+        self.raw = item.raw
+        self.remove_duplicates = remove_duplicates
+
+class StringConfigOption(ConfigOption):
+    """A ConfigOption that is parsed into a string.
+
+    If null==True, a value of 'None' will be parsed in to None instead of
+    just leaving it as the string 'None'.
+    """
+
+    def _get_default(self):
+        return '' if not self.null else None
+
+    def parse(self, value, raw=False):
+        if raw:
+            return value
+
+        return unicode(value)
+
+    def __init__(self, raw=False, default=NO_DEFAULT, fatal=False, null=False,
+                 help='', action='store'):
+        self.null = null
+        super(StringConfigOption, self).__init__(raw=raw, default=default,
+            fatal=fatal, help=help, action=action)
+
+
+class TupleConfigOption(ConfigOption):
+    """A ConfigOption that is parsed into a fixed-size tuple of strings.
+
+    The number of items in the tuple should be specified with the 'length'
+    constructor argument.
+    """
+
+    def __init__(self, length=0, raw=False, default=NO_DEFAULT, fatal=False,
+                 help='', action='store'):
+        super(TupleConfigOption, self).__init__(raw=raw, default=default,
+            fatal=fatal, help=help, action=action)
+        self.length = length
+
+    def _get_default(self):
+        return ()
+
+    def parse(self, value, raw=False):
+        parts = [part.strip() for part in value.split(',')]
+        if parts == ['()']:
+            result = ()
+        elif self.length:
+            # length is not 0, so length validation
+            if len(parts) == self.length:
+                result = tuple(parts)
+            else:
+                raise ValueError("Tuples need to be %d items long" % self.length)
+        else:
+            result = tuple(parts)
+            # length is 0, so no length validation
+        return result
+
+
+class DictConfigOption(ConfigOption):
+    """A ConfigOption that is parsed into a dictionary.
+
+    In the configuration file you'll need to specify the name of a section,
+    and all that section's items will be parsed as a dictionary.
+
+    The available keys for the dict are specified with the 'spec' constructor
+    argument, that should be in turn a dictionary.  spec's keys are the
+    available keys for the config file, and spec's values should be
+    ConfigOptions that will be used to parse the values in the config file.
+    """
+    require_parser = True
+
+    def __init__(self, spec=None, strict=False, raw=False,
+                 default=NO_DEFAULT, fatal=False, help='', action='store',
+                 item=None):
+        if spec is None:
+            spec = {}
+        if item is None:
+            item = StringConfigOption()
+        self.spec = spec
+        self.strict = strict
+        self.item = item
+        super(DictConfigOption, self).__init__(raw=raw, default=default,
+            fatal=fatal, help=help, action=action)
+
+    def _get_default(self):
+        default = {}
+        for key, value in self.spec.items():
+            default[key] = value.default
+        return default
+
+    def parse(self, section, parser=None, raw=False):
+        parsed = dict(parser.items(section))
+        result = {}
+
+        # parse config items according to spec
+        for key, value in parsed.items():
+            if self.strict and not key in self.spec:
+                raise ValueError("Invalid key %s in section %s" % (key,
+                                                                   section))
+            option = self.spec.get(key, None)
+            if option is None:
+                # option not part of spec, but we are in non-strict mode
+                # parse it using the default item parser
+                option = self.item
+
+            # parse option
+            kwargs = {}
+            if option.require_parser:
+                kwargs['parser'] = parser
+            if not raw:
+                value = option.parse(value, **kwargs)
+            result[key] = value
+
+        # fill in missing items with default values
+        for key in self.spec:
+            if not key in parsed:
+                option = self.spec[key]
+                if option.fatal:
+                    raise ValueError("No option '%s' in section '%s'" %
+                        (key, section))
+                else:
+                    if not raw:
+                        value = option.default
+                    else:
+                        value = unicode(option.default)
+                    result[key] = value
+        return result
+
+    def get_extra_sections(self, section, parser):
+        sections = []
+        for option in parser.options(section):
+            option_obj = self.spec.get(option, self.item)
+            is_dict_item = isinstance(option_obj, DictConfigOption)
+            is_dict_lines_item = (hasattr(option_obj, 'item') and
+                isinstance(option_obj.item, DictConfigOption))
+
+            if is_dict_item:
+                base = option_obj
+            elif is_dict_lines_item:
+                base = option_obj.item
+            else:
+                continue
+
+            value = parser.get(section, option, parse=False)
+            names = value.split()
+            sections.extend(names)
+
+            # recurse
+            for name in names:
+                extra = base.get_extra_sections(name, parser)
+                sections.extend(extra)
+
+        return sections
+

=== added file 'configglue/pyschema/parser.py'
--- configglue/pyschema/parser.py	1970-01-01 00:00:00 +0000
+++ configglue/pyschema/parser.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,504 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+import codecs
+import collections
+import copy
+import os
+import string
+
+from ConfigParser import (NoSectionError, DEFAULTSECT,
+    InterpolationMissingOptionError, NoOptionError,
+    ConfigParser as BaseConfigParser)
+
+from configglue.pyschema.options import DictConfigOption
+
+
+CONFIG_FILE_ENCODING = 'utf-8'
+
+
+class SchemaValidationError(Exception):
+    pass
+
+
+class SchemaConfigParser(BaseConfigParser, object):
+    """A ConfigParser that validates against a Schema
+
+    The way to use this class is:
+
+    config = SchemaConfigParser(MySchema())
+    config.read('mysystemconfig.cfg', 'mylocalconfig.cfg', ...)
+    config.parse_all()
+    ...
+    profit!
+    """
+    def __init__(self, schema):
+        super(SchemaConfigParser, self).__init__()
+        # validate schema
+        if not schema.is_valid():
+            # TODO: add error details
+            raise SchemaValidationError()
+        self.schema = schema
+        self._location = {}
+        self.extra_sections = set()
+        self._basedir = ''
+        self._dirty = collections.defaultdict(
+            lambda: collections.defaultdict(dict))
+
+    def is_valid(self, report=False):
+        valid = True
+        errors = []
+        try:
+            # validate structure
+            parsed_sections = set(self.sections())
+            schema_sections = set(s.name for s in self.schema.sections())
+            skip_sections = self.extra_sections
+            if '__noschema__' in parsed_sections:
+                skip_sections.add('__noschema__')
+            schema_sections.update(skip_sections)
+            sections_match = (parsed_sections == schema_sections)
+            if not sections_match:
+                error_msg = "Sections in configuration do not match schema: %s"
+                unmatched_sections = list(parsed_sections - schema_sections)
+                error_value = ', '.join(unmatched_sections)
+                errors.append(error_msg % error_value)
+            valid &= sections_match
+
+            for name in parsed_sections:
+                if name not in skip_sections:
+                    if not self.schema.has_section(name):
+                        # this should have been reported before
+                        # so skip bogus section
+                        continue
+
+                    section = self.schema.section(name)
+                    parsed_options = set(self.options(name))
+                    schema_options = set(section.options())
+
+                    fatal_options = set(opt.name for opt in schema_options
+                                        if opt.fatal)
+                    # all fatal options are included
+                    fatal_included = parsed_options.issuperset(fatal_options)
+                    if not fatal_included:
+                        error_msg = ("Configuration missing required options"
+                                     " for section '%s': %s")
+                        error_value = ', '.join(list(fatal_options -
+                                                     parsed_options))
+                        errors.append(error_msg % (name, error_value))
+                    valid &= fatal_included
+
+                    # remaining parsed options are valid schema options
+                    other_options = parsed_options - fatal_options
+                    schema_opt_names = set(opt.name for opt in schema_options)
+
+                    # add the default section special includes option
+                    if name == '__main__':
+                        schema_opt_names.add('includes')
+
+                    schema_options = other_options.issubset(schema_opt_names)
+                    if not schema_options:
+                        error_msg = ("Configuration includes invalid options"
+                                     " for section '%s': %s")
+                        error_value = ', '.join(list(other_options -
+                                                     schema_opt_names))
+                        errors.append(error_msg % (name, error_value))
+                    valid &= schema_options
+
+            # structure validates, validate content
+            self.parse_all()
+
+        except Exception, e:
+            if valid:
+                errors.append(e)
+                valid = False
+
+        if report:
+            return valid, errors
+        else:
+            return valid
+
+    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.
+        """
+        d = self._defaults.copy()
+        try:
+            d.update(self._sections[section])
+        except KeyError:
+            if section != DEFAULTSECT:
+                raise NoSectionError(section)
+        # Update with the entry specific variables
+        if vars:
+            for key, value in vars.items():
+                d[self.optionxform(key)] = value
+        options = d.keys()
+        if "__name__" in options:
+            options.remove("__name__")
+        if raw:
+            return [(option, d[option])
+                    for option in options]
+        else:
+            items = []
+            for option in options:
+                try:
+                    value = self._interpolate(section, option, d[option], d)
+                except InterpolationMissingOptionError, e:
+                    # interpolation failed, because key was not found in
+                    # section. try other sections before bailing out
+                    value = self._interpolate_value(section, option)
+                    if value is None:
+                        # this should be a string, so None indicates an error
+                        raise e
+                items.append((option, value))
+            return items
+
+    def values(self, section=None, parse=True):
+        """Returns multiple values, in a dict.
+
+        This method can return the value of multiple options in a single call,
+        unlike get() that returns a single option's value.
+
+        If section=None, return all options from all sections.
+        If section is specified, return all options from that section only.
+
+        Section is to be specified *by name*, not by
+        passing in real ConfigSection objects.
+        """
+        values = collections.defaultdict(dict)
+        if section is None:
+            sections = self.schema.sections()
+        else:
+            sections = [self.schema.section(section)]
+
+        for sect in sections:
+            for opt in sect.options():
+                values[sect.name][opt.name] = self.get(
+                    sect.name, opt.name, parse=parse)
+        if section is not None:
+            return values[section]
+        else:
+            return values
+
+    def read(self, filenames, already_read=None):
+        """Like ConfigParser.read, but consider files we've already read."""
+        if already_read is None:
+            already_read = set()
+        if isinstance(filenames, basestring):
+            filenames = [filenames]
+        read_ok = []
+        for filename in filenames:
+            path = os.path.join(self._basedir, filename)
+            if path in already_read:
+                continue
+            try:
+                fp = codecs.open(path, 'r', CONFIG_FILE_ENCODING)
+            except IOError:
+                continue
+            self._read(fp, path, already_read=already_read)
+            fp.close()
+            read_ok.append(path)
+        return read_ok
+
+    def readfp(self, fp, filename=None):
+        # wrap the StringIO so it can read encoded text
+        decoded_fp = codecs.getreader(CONFIG_FILE_ENCODING)(fp)
+        self._read(decoded_fp, filename)
+
+    def _read(self, fp, fpname, already_read=None):
+        # read file content
+        self._update(fp, fpname)
+
+        if already_read is None:
+            already_read = set()
+        already_read.add(fpname)
+
+        if self.has_option('__main__', 'includes'):
+            old_basedir, self._basedir = self._basedir, os.path.dirname(fpname)
+            includes = self.get('__main__', 'includes')
+            filenames = map(string.strip, includes)
+            for filename in filenames:
+                self.read(filename, already_read=already_read)
+            self._basedir = old_basedir
+
+            if filenames:
+                # re-read the file to override included options with
+                # local values
+                fp.seek(0)
+                self._update(fp, fpname)
+
+    def _update(self, fp, fpname):
+        # remember current values
+        old_sections = copy.deepcopy(self._sections)
+        # read in new file
+        super(SchemaConfigParser, self)._read(fp, fpname)
+        # update location of changed values
+        self._update_location(old_sections, fpname)
+
+    def _update_location(self, old_sections, filename):
+        # keep list of valid options to include locations for
+        option_names = map(lambda x: x.name, self.schema.options())
+
+        # new values
+        sections = self._sections
+
+        # update locations
+        for section, options in sections.items():
+            old_section = old_sections.get(section)
+            if old_section is not None:
+                # update options in existing section
+                for option, value in options.items():
+                    valid_option = option in option_names
+                    option_changed = (option not in old_section or
+                                      value != old_section[option])
+                    if valid_option and option_changed:
+                        self._location[option] = filename
+            else:
+                # complete section is new
+                for option, value in options.items():
+                    valid_option = option in option_names
+                    if valid_option:
+                        self._location[option] = filename
+
+    def parse(self, section, option, value):
+        if section == '__main__':
+            option_obj = getattr(self.schema, option, None)
+        else:
+            section_obj = getattr(self.schema, section, None)
+            if section_obj is not None:
+                option_obj = getattr(section_obj, option, None)
+            else:
+                raise NoSectionError(section)
+
+        if option_obj is not None:
+            kwargs = {}
+            if option_obj.require_parser:
+                kwargs = {'parser': self}
+
+            # hook to save extra sections
+            is_dict_option = isinstance(option_obj, DictConfigOption)
+            is_dict_lines_option = (hasattr(option_obj, 'item') and
+                isinstance(option_obj.item, DictConfigOption))
+            is_default_value = unicode(option_obj.default) == value
+
+            # avoid adding implicit sections for dict default value
+            if ((is_dict_option or is_dict_lines_option) and
+                not is_default_value):
+                sections = value.split()
+                self.extra_sections.update(set(sections))
+
+                if is_dict_option:
+                    base = option_obj
+                else:
+                    base = option_obj.item
+
+                for name in sections:
+                    nested = base.get_extra_sections(name, self)
+                    self.extra_sections.update(set(nested))
+
+            if is_default_value:
+                value = option_obj.default
+            else:
+                try:
+                    value = option_obj.parse(value, **kwargs)
+                except ValueError, e:
+                    raise ValueError("Invalid value '%s' for %s '%s' in"
+                        " section '%s'. Original exception was: %s" %
+                        (value, option_obj.__class__.__name__, option,
+                         section, e))
+        return value
+
+    def parse_all(self):
+        """Go through all sections and options attempting to parse each one.
+
+        If any options are omitted from the config file, provide the
+        default value from the schema.  If the option has fatal=True, raise
+        an exception.
+        """
+        for section in self.schema.sections():
+            for option in section.options():
+                try:
+                    self.get(section.name, option.name, raw=option.raw)
+                except (NoSectionError, NoOptionError):
+                    if option.fatal:
+                        raise
+
+    def locate(self, option=None):
+        return self._location.get(option)
+
+    def _get_interpolation_keys(self, section, option):
+        def extract_keys(item):
+            if isinstance(item, (list, tuple)):
+                keys = map(extract_keys, item)
+                keys = reduce(set.union, keys, set())
+            else:
+                keys = set(self._KEYCRE.findall(item))
+            # remove invalid key
+            if '' in keys:
+                keys.remove('')
+            return keys
+
+        rawval = super(SchemaConfigParser, self).get(section, option, True)
+        try:
+            opt = self.schema.section(section).option(option)
+            value = opt.parse(rawval, raw=True)
+        except:
+            value = rawval
+
+        keys = extract_keys(value)
+        return rawval, keys
+
+    def _interpolate_value(self, section, option):
+        rawval, keys = self._get_interpolation_keys(section, option)
+        if not keys:
+            # interpolation keys are not valid
+            return
+
+        values = {}
+        # iterate over the other sections
+        for key in keys:
+            # we want the unparsed value
+            try:
+                value = self.get(section, key, parse=False)
+            except (NoSectionError, NoOptionError):
+                # value of key not found in config, so try in special
+                # sections
+                for section in ('__main__', '__noschema__'):
+                    try:
+                        value = super(SchemaConfigParser, self).get(section,
+                                                                    key)
+                        break
+                    except:
+                        continue
+                else:
+                    return
+            values[key] = value
+
+        # replace holders with values
+        result = rawval % values
+
+        assert isinstance(result, basestring)
+        return result
+
+    def _get_default(self, section, option):
+        # mark the value as not initialized to be able to have a None default
+        marker = object()
+        value = marker
+
+        # cater for 'special' sections
+        if section == '__main__':
+            opt = getattr(self.schema, option, None)
+            if opt is not None and not opt.fatal:
+                value = opt.default
+        elif section == '__noschema__':
+            value = super(SchemaConfigParser, self).get(section, option)
+        else:
+            try:
+                opt = self.schema.section(section).option(option)
+                if not opt.fatal:
+                    value = opt.default
+            except Exception:
+                pass
+
+        if value is marker:
+            # value was not set, so either section or option was not found
+            # or option was required (fatal set to True)
+            #if self.schema.has_section(section):
+            #    raise NoOptionError(option, section)
+            #else:
+            #    raise NoSectionError(section)
+            return None
+        else:
+            # we want to return a non-parsed value
+            # a unicode of the value is the closest we can get
+            return unicode(value)
+
+    def get(self, section, option, raw=False, vars=None, parse=True):
+        try:
+            # get option's raw mode setting
+            try:
+                section_obj = self.schema.section(section)
+                option_obj = section_obj.option(option)
+                raw = option_obj.raw or raw
+            except:
+                pass
+            # value is defined entirely in current section
+            value = super(SchemaConfigParser, self).get(section, option,
+                                                        raw, vars)
+        except InterpolationMissingOptionError, e:
+            # interpolation key not in same section
+            value = self._interpolate_value(section, option)
+            if value is None:
+                # this should be a string, so None indicates an error
+                raise e
+        except (NoSectionError, NoOptionError), e:
+            # option not found in config, try to get its default value from
+            # schema
+            value = self._get_default(section, option)
+            if value is None:
+                raise
+
+            # value found, so section and option exist
+            # add it to the config
+            if not self.has_section(section):
+                # Don't call .add_section here because 2.6 complains
+                # about sections called '__main__'
+                self._sections[section] = {}
+            self.set(section, option, value)
+
+        if parse:
+            value = self.parse(section, option, value)
+        return value
+
+    def set(self, section, option, value):
+        super(SchemaConfigParser, self).set(section, option, value)
+        filename = self.locate(option)
+        self._dirty[filename][section][option] = value
+
+    def save(self, fp=None):
+        if fp is not None:
+            if isinstance(fp, basestring):
+                fp = open(fp, 'w')
+            # write to a specific file
+            encoded_fp = codecs.getwriter(CONFIG_FILE_ENCODING)(fp)
+            self.write(encoded_fp)
+        else:
+            # write to the original files
+            for filename, sections in self._dirty.items():
+
+                parser = BaseConfigParser()
+                parser.read(filename)
+
+                for section, options in sections.items():
+                    for option, value in options.items():
+                        parser.set(section, option, value)
+
+                # write to new file
+                parser.write(open("%s.new" % filename, 'w'))
+                # rename old file
+                if os.path.exists(filename):
+                    os.rename(filename, "%s.old" % filename)
+                # rename new file
+                os.rename("%s.new" % filename, filename)
+

=== added file 'configglue/pyschema/schema.py'
--- configglue/pyschema/schema.py	1970-01-01 00:00:00 +0000
+++ configglue/pyschema/schema.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,106 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+from configglue.pyschema import ConfigOption, ConfigSection, super_vars
+from configglue.pyschema.options import LinesConfigOption, StringConfigOption
+
+
+class Schema(object):
+    """A complete description of a system configuration.
+
+    To define your own configuration schema you should:
+     1- Inherit from Schema
+     2- Add ConfigOptions and ConfigSections as class attributes.
+
+    With that your whole configuration schema is defined, and you can now
+    load configuration files.
+
+    ConfigOptions that don't go in a ConfigSection will belong in the
+    '__main__' section of the configuration files.
+
+    One ConfigOption comes already defined in Schema, 'includes' in the
+    '__main__' section, that allows configuration files to include other
+    configuration files.
+    """
+    def __init__(self):
+        self.includes = LinesConfigOption(item=StringConfigOption())
+        self._sections = {}
+        defaultSection = None
+        for attname in super_vars(self.__class__):
+            att = getattr(self, attname)
+            if isinstance(att, ConfigSection):
+                att.name = attname
+                self._sections[attname] = att
+                for optname in super_vars(att):
+                    opt = getattr(att, optname)
+                    if isinstance(opt, ConfigOption):
+                        opt.name = optname
+                        opt.section = att
+            elif isinstance(att, ConfigOption):
+                if defaultSection is None:
+                    defaultSection = ConfigSection()
+                    defaultSection.name = '__main__'
+                    self._sections['__main__'] = defaultSection
+                att.name = attname
+                att.section = defaultSection
+                setattr(defaultSection, attname, att)
+
+    def __eq__(self, other):
+        return (self._sections == other._sections and
+                self.includes == other.includes)
+
+    def is_valid(self):
+        explicit_default_section = isinstance(getattr(self, '__main__', None),
+                                              ConfigSection)
+        is_valid = not explicit_default_section
+        return is_valid
+
+    def has_section(self, name):
+        """Return True if a ConfigSection with the given name is available"""
+        return name in self._sections.keys()
+
+    def section(self, name):
+        """Return a ConfigSection by name"""
+        section = self._sections.get(name)
+        assert section is not None, "Invalid ConfigSection name '%s'" % name
+        return section
+
+    def sections(self):
+        """Returns the list of available ConfigSections"""
+        return self._sections.values()
+
+    def options(self, section=None):
+        """Return all the ConfigOptions within a given section.
+
+        If section is omitted, returns all the options in the configuration
+        file, flattening out any sections.
+        To get options from the default section, specify section='__main__'
+        """
+        if isinstance(section, basestring):
+            section = self.section(section)
+        if section is None:
+            options = []
+            for s in self.sections():
+                options += self.options(s)
+        elif section.name == '__main__':
+            options = [getattr(self, att) for att in super_vars(self.__class__) 
+                           if isinstance(getattr(self, att), ConfigOption)]
+        else:
+            options = section.options()
+        return options
+
+

=== modified file 'setup.py'
--- setup.py	2009-06-15 18:23:08 +0000
+++ setup.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,20 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
 
 from setuptools import setup, find_packages
 import sys, os
@@ -29,5 +42,5 @@
       packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
       include_package_data=True,
       zip_safe=True,
-      test_suite='configglue.tests',
+      test_suite='tests',
       )

=== renamed directory 'configglue/tests' => 'tests'
=== added file 'tests/__init__.py'
--- tests/__init__.py	1970-01-01 00:00:00 +0000
+++ tests/__init__.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,17 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+

=== added directory 'tests/inischema'
=== renamed file 'configglue/tests/__init__.py' => 'tests/inischema/__init__.py'
--- configglue/tests/__init__.py	2009-03-19 20:01:36 +0000
+++ tests/inischema/__init__.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 """Tests! Who woulda said"""
 # Two use cases so far for this file:

=== renamed file 'configglue/tests/test_attributed.py' => 'tests/inischema/test_attributed.py'
--- configglue/tests/test_attributed.py	2009-03-19 20:01:36 +0000
+++ tests/inischema/test_attributed.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 # in testfiles, putting docstrings on methods messes up with the
 # runner's output, so pylint: disable-msg=C0111
@@ -10,7 +22,7 @@
 from ConfigParser import RawConfigParser
 from StringIO import StringIO
 
-from configglue.attributed import AttributedConfigParser
+from configglue.inischema.attributed import AttributedConfigParser
 
 class BaseTest(unittest.TestCase):
     """ Base class to keep common set-up """

=== renamed file 'configglue/tests/test_glue.py' => 'tests/inischema/test_glue.py'
--- configglue/tests/test_glue.py	2009-03-19 20:01:36 +0000
+++ tests/inischema/test_glue.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 # in testfiles, putting docstrings on methods messes up with the
 # runner's output, so pylint: disable-msg=C0111
@@ -10,29 +22,33 @@
 import unittest
 from StringIO import StringIO
 
-from configglue.glue import configglue
+from configglue.inischema.glue import configglue
 
 class TestBase(unittest.TestCase):
     """ Base class to keep common set-up """
     def setUp(self):
         self.file = StringIO(self.ini)
+        self.old_sys_argv = sys.argv
+        sys.argv = ['']
+
+    def tearDown(self):
+        sys.argv = self.old_sys_argv
 
 class TestGlue(TestBase):
     ini = '''
 [blah]
-foo.default = 3
 foo.help = yadda yadda yadda
          yadda
 foo.metavar = FOO
 foo.parser = int
 foo = 2
 '''
-    arg = '--blah-foo'
+    arg = '--blah_foo'
     opt = 'blah_foo'
     val = 2
 
     def test_ini_file_wins_when_no_args(self):
-        parser, options, args = configglue(self.file)
+        parser, options, args = configglue(self.file, args=[])
         self.assertEqual(vars(options),
                          {self.opt: self.val})
 
@@ -40,7 +56,7 @@
         parser, options, args = configglue(self.file,
                                            args=['', self.arg + '=5'])
         self.assertEqual(vars(options),
-                         {self.opt: 5})
+                         {self.opt: '5'})
     def test_help_is_displayed(self):
         sys.stdout = StringIO()
         try:
@@ -60,17 +76,17 @@
 foo.parser = int
 foo = 2
 '''
-    arg = '--bl-ah-foo'
+    arg = '--bl-ah_foo'
     opt = 'bl_ah_foo'
 
 class TestNoValue(TestGlue):
     ini = '''
 [blah]
-foo.default = 3
 foo.help = yadda yadda yadda
          yadda
 foo.metavar = FOO
 foo.parser = int
+foo = 3
 '''
     val = 3
 
@@ -84,11 +100,11 @@
     ini = '[x]\na.help=hi\n'
     def test_empty(self):
         parser, options, args = configglue(self.file)
-        self.assertEqual(options.x_a, None)
+        self.assertEqual(options.x_a, '')
 
     def test_accepts_args_and_filenames(self):
         parser, options, args = configglue(self.file, 'dummy',
-                                           args=['', '--x-a=1'])
+                                           args=['', '--x_a=1'])
         self.assertEqual(options.x_a, '1')
 
 class TestGlueBool(TestBase):
@@ -121,7 +137,7 @@
 '''
     def test_nothing(self):
         parser, options, args = configglue(self.file)
-        self.assertEqual(options.foo, None)
+        self.assertEqual(options.foo, [])
 
     def test_no_append(self):
         parser, options, args = configglue(self.file)

=== renamed file 'configglue/tests/test_parsers.py' => 'tests/inischema/test_parsers.py'
--- configglue/tests/test_parsers.py	2009-03-19 20:01:36 +0000
+++ tests/inischema/test_parsers.py	2010-08-04 21:21:41 +0000
@@ -1,14 +1,26 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 # in testfiles, putting docstrings on methods messes up with the
 # runner's output, so pylint: disable-msg=C0111
 
 import unittest
 
-from configglue import parsers
+from configglue.inischema import parsers
 
 class TestParsers(unittest.TestCase):
     def test_bool(self):

=== renamed file 'configglue/tests/test_typed.py' => 'tests/inischema/test_typed.py'
--- configglue/tests/test_typed.py	2010-07-29 13:13:21 +0000
+++ tests/inischema/test_typed.py	2010-08-04 21:21:41 +0000
@@ -1,7 +1,19 @@
-# This file is part of configglue, by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
-# (C) 2009 by Canonical Ltd.
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
 # Released under the BSD License (see the file LICENSE)
+# 
 # For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
 
 # in testfiles, putting docstrings on methods messes up with the
 # runner's output, so pylint: disable-msg=C0111
@@ -10,7 +22,7 @@
 from StringIO import StringIO
 from ConfigParser import RawConfigParser
 
-from configglue.typed import TypedConfigParser
+from configglue.inischema.typed import TypedConfigParser
 
 marker = object()
 def some_parser(value):

=== added directory 'tests/pyschema'
=== added file 'tests/pyschema/__init__.py'
--- tests/pyschema/__init__.py	1970-01-01 00:00:00 +0000
+++ tests/pyschema/__init__.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,16 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################

=== added file 'tests/pyschema/test_glue2glue.py'
--- tests/pyschema/test_glue2glue.py	1970-01-01 00:00:00 +0000
+++ tests/pyschema/test_glue2glue.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+import sys
+import unittest
+from StringIO import StringIO
+
+from configglue.inischema import configglue
+from configglue.pyschema import schemaconfigglue, ini2schema
+
+
+class TestGlueConvertor(unittest.TestCase):
+    def setUp(self):
+        # make sure we have a clean sys.argv so as not to have unexpected test
+        # results
+        self.old_argv = sys.argv
+        sys.argv = []
+
+    def tearDown(self):
+        # restore old sys.argv
+        sys.argv = self.old_argv
+
+    def test_empty(self):
+        s = ""
+        _, cg, _ = configglue(StringIO(s))
+        _, sg, _ = schemaconfigglue(ini2schema(StringIO(s)))
+        self.assertEqual(vars(cg), vars(sg))
+
+    def test_simple(self):
+        s = "[foo]\nbar = 42\n"
+        _, cg, _ = configglue(StringIO(s))
+        _, sg, _ = schemaconfigglue(ini2schema(StringIO(s)))
+        self.assertEqual(vars(cg), vars(sg))
+
+    def test_main(self):
+        s = "[__main__]\nbar = 42\n"
+        _, cg, _ = configglue(StringIO(s))
+        _, sg, _ = schemaconfigglue(ini2schema(StringIO(s)))
+        self.assertEqual(vars(cg), vars(sg))
+
+    def test_parser_none(self):
+        s = "[__main__]\nbar = meeeeh\nbar.parser = none"
+        _, cg, _ = configglue(StringIO(s),
+                              extra_parsers=[('none', str)])
+        _, sg, _ = schemaconfigglue(ini2schema(StringIO(s)))
+        self.assertEqual(vars(cg), vars(sg))
+
+    def test_parser_unicode(self):
+        s = "[__main__]\nbar = zátrapa\nbar.parser = unicode\nbar.parser.args = utf-8"
+        _, cg, _ = configglue(StringIO(s))
+        _, sg, _ = schemaconfigglue(ini2schema(StringIO(s)))
+        self.assertEqual(vars(cg), vars(sg))
+
+    def test_parser_int(self):
+        s = "[__main__]\nbar = 42\nbar.parser = int\n"
+        _, cg, _ = configglue(StringIO(s))
+        _, sg, _ = schemaconfigglue(ini2schema(StringIO(s)))
+        self.assertEqual(vars(cg), vars(sg))
+
+    def test_parser_bool(self):
+        s = "[__main__]\nbar = true\nbar.parser = bool \n"
+        _, cg, _ = configglue(StringIO(s))
+        _, sg, _ = schemaconfigglue(ini2schema(StringIO(s)))
+        self.assertEqual(vars(cg), vars(sg))
+
+if __name__ == '__main__':
+
+    unittest.main()

=== added file 'tests/pyschema/test_options.py'
--- tests/pyschema/test_options.py	1970-01-01 00:00:00 +0000
+++ tests/pyschema/test_options.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,511 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+import unittest
+from StringIO import StringIO
+
+from configglue.pyschema.options import (BoolConfigOption, IntConfigOption,
+    StringConfigOption, LinesConfigOption, TupleConfigOption, DictConfigOption)
+from configglue.pyschema.parser import SchemaConfigParser
+from configglue.pyschema.schema import Schema
+
+
+class TestStringConfigOption(unittest.TestCase):
+    def test_init_no_args(self):
+        opt = StringConfigOption()
+        self.assertFalse(opt.null)
+
+    def test_init_null(self):
+        opt = StringConfigOption(null=True)
+        self.assertTrue(opt.null)
+
+    def test_parse_string(self):
+        class MySchema(Schema):
+            foo = StringConfigOption(null=True)
+        config = StringIO("[__main__]\nfoo = 42")
+        expected_values = {'__main__': {'foo': '42'}}
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+        config = StringIO("[__main__]\nfoo = ")
+        expected_values = {'__main__': {'foo': ''}}
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+        config = StringIO("[__main__]\nfoo = None")
+        expected_values = {'__main__': {'foo': None}}
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+        class MySchema(Schema):
+            foo = StringConfigOption()
+        config = StringIO("[__main__]\nfoo = None")
+        expected_values = {'__main__': {'foo': 'None'}}
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_default(self):
+        opt = StringConfigOption()
+        self.assertEqual(opt.default, '')
+
+    def test_default_null(self):
+        opt = StringConfigOption(null=True)
+        self.assertEqual(opt.default, None)
+
+
+class TestIntConfigOption(unittest.TestCase):
+    def test_parse_int(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+        config = StringIO("[__main__]\nfoo = 42")
+        expected_values = {'__main__': {'foo': 42}}
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+        config = StringIO("[__main__]\nfoo =")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.values)
+
+        config = StringIO("[__main__]\nfoo = bla")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.values)
+
+    def test_default(self):
+        opt = IntConfigOption()
+        self.assertEqual(opt.default, 0)
+
+
+class TestBoolConfigOption(unittest.TestCase):
+    def test_parse_bool(self):
+        class MySchema(Schema):
+            foo = BoolConfigOption()
+        config = StringIO("[__main__]\nfoo = Yes")
+        expected_values = {'__main__': {'foo': True}}
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+        config = StringIO("[__main__]\nfoo = tRuE")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+        config = StringIO("[__main__]\nfoo =")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.values)
+
+        config = StringIO("[__main__]\nfoo = bla")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.values)
+
+    def test_default(self):
+        opt = BoolConfigOption()
+        self.assertEqual(opt.default, False)
+
+
+class TestLinesConfigOption(unittest.TestCase):
+    def test_parse_int_lines(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(item=IntConfigOption())
+
+        config = StringIO("[__main__]\nfoo = 42\n 43\n 44")
+        expected_values = {'__main__': {'foo': [42, 43, 44]}}
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_bool_lines(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(item=BoolConfigOption())
+        schema = MySchema()
+        config = StringIO("[__main__]\nfoo = tRuE\n No\n 0\n 1")
+        expected_values = {'__main__': {'foo': [True, False, False, True]}}
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(expected_values, parser.values())
+
+    def test_parse_bool_empty_lines(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(item=BoolConfigOption())
+        schema = MySchema()
+        config = StringIO("[__main__]\nfoo =")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        expected_values = {'__main__': {'foo': []}}
+        self.assertEqual(expected_values, parser.values())
+
+    def test_parse_bool_invalid_lines(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(item=BoolConfigOption())
+        schema = MySchema()
+        config = StringIO("[__main__]\nfoo = bla")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.values)
+
+        config = StringIO("[__main__]\nfoo = True\n bla")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.values)
+
+    def test_default(self):
+        opt = LinesConfigOption(item=IntConfigOption())
+        self.assertEqual(opt.default, [])
+
+    def test_remove_duplicates(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(item=StringConfigOption(),
+                                    remove_duplicates=True)
+        schema = MySchema()
+        config = StringIO("[__main__]\nfoo = bla\n blah\n bla")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEquals({'__main__': {'foo': ['bla', 'blah']}},
+                          parser.values())
+
+    def test_remove_dict_duplicates(self):
+        class MyOtherSchema(Schema):
+            foo = LinesConfigOption(item=DictConfigOption(),
+                                    remove_duplicates=True)
+        schema = MyOtherSchema()
+        config = StringIO("[__main__]\nfoo = bla\n bla\n[bla]\nbar = baz")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEquals({'__main__': {'foo': [{'bar': 'baz'}]}},
+                          parser.values())
+
+class TestTupleConfigOption(unittest.TestCase):
+    def test_init(self):
+        opt = TupleConfigOption(2)
+        self.assertEqual(opt.length, 2)
+
+    def test_init_no_length(self):
+        opt = TupleConfigOption()
+        self.assertEqual(opt.length, 0)
+        self.assertEqual(opt.default, ())
+
+    def test_parse_no_length(self):
+        class MySchema(Schema):
+            foo = TupleConfigOption()
+
+        config = StringIO('[__main__]\nfoo=1,2,3,4')
+        expected_values = {'__main__': {'foo': ('1', '2', '3', '4')}}
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_tuple(self):
+        class MySchema(Schema):
+            foo = TupleConfigOption(length=4)
+        config = StringIO('[__main__]\nfoo = 1, 2, 3, 4')
+        expected_values = {'__main__': {'foo': ('1', '2', '3', '4')}}
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+        config = StringIO('[__main__]\nfoo = 1, 2, 3')
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.values)
+
+        config = StringIO('[__main__]\nfoo = ')
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.values)
+
+    def test_default(self):
+        opt = TupleConfigOption(2)
+        self.assertEqual(opt.default, ())
+
+
+class TestDictConfigOption(unittest.TestCase):
+    def test_init(self):
+        opt = DictConfigOption()
+        self.assertEqual(opt.spec, {})
+        self.assertEqual(opt.strict, False)
+
+        spec = {'a': IntConfigOption(), 'b': BoolConfigOption()}
+        opt = DictConfigOption(spec)
+        self.assertEqual(opt.spec, spec)
+        self.assertEqual(opt.strict, False)
+
+        opt = DictConfigOption(spec, strict=True)
+        self.assertEqual(opt.spec, spec)
+        self.assertEqual(opt.strict, True)
+
+    def test_get_extra_sections(self):
+        class MySchema(Schema):
+            foo = DictConfigOption(item=DictConfigOption())
+
+        config = StringIO("""
+[__main__]
+foo=dict1
+[dict1]
+bar=dict2
+[dict2]
+baz=42
+""")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        expected = ['dict2']
+
+        opt = DictConfigOption(item=DictConfigOption())
+        extra = opt.get_extra_sections('dict1', parser)
+        self.assertEqual(extra, expected)
+
+    def test_parse_dict(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': StringConfigOption(),
+                                    'baz': IntConfigOption(),
+                                    'bla': BoolConfigOption(),
+                                    })
+        config = StringIO("""[__main__]
+foo = mydict
+[mydict]
+bar=baz
+baz=42
+bla=Yes
+""")
+        expected_values = {
+            '__main__': {
+                'foo': {'bar': 'baz', 'baz': 42, 'bla': True}
+            }
+        }
+
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_raw(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': StringConfigOption(),
+                                    'baz': IntConfigOption(),
+                                    'bla': BoolConfigOption(),
+                                    })
+        config = StringIO("""[__main__]
+foo = mydict
+[mydict]
+baz=42
+""")
+        expected = {'bar': '', 'baz': '42', 'bla': 'False'}
+
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        parsed = schema.foo.parse('mydict', parser, True)
+        self.assertEqual(parsed, expected)
+
+    def test_parse_invalid_key_in_parsed(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': IntConfigOption()})
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]\nbaz=2")
+        expected_values = {'__main__': {'foo': {'bar': 0, 'baz': '2'}}}
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_invalid_key_in_spec(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': IntConfigOption(),
+                                    'baz': IntConfigOption(fatal=True)})
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]\nbar=2")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.parse_all)
+
+    def test_default(self):
+        opt = DictConfigOption({})
+        self.assertEqual(opt.default, {})
+
+    def test_parse_no_strict_missing_args(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': IntConfigOption()})
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]")
+        expected_values = {'__main__': {'foo': {'bar': 0}}}
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_no_strict_extra_args(self):
+        class MySchema(Schema):
+            foo = DictConfigOption()
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]\nbar=2")
+        expected_values = {'__main__': {'foo': {'bar': '2'}}}
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_no_strict_with_item(self):
+        class MySchema(Schema):
+            foo = DictConfigOption(
+                      item=DictConfigOption(
+                          item=IntConfigOption()))
+        config = StringIO("""
+[__main__]
+foo = mydict
+[mydict]
+bar = baz
+[baz]
+wham=42
+""")
+        expected_values = {'__main__': {'foo': {'bar': {'wham': 42}}}}
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_strict(self):
+        class MySchema(Schema):
+            spec = {'bar': IntConfigOption()}
+            foo = DictConfigOption(spec, strict=True)
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]\nbar=2")
+        expected_values = {'__main__': {'foo': {'bar': 2}}}
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_strict_missing_vars(self):
+        class MySchema(Schema):
+            spec = {'bar': IntConfigOption(),
+                    'baz': IntConfigOption()}
+            foo = DictConfigOption(spec, strict=True)
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]\nbar=2")
+        expected_values = {'__main__': {'foo': {'bar': 2, 'baz': 0}}}
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_parse_strict_extra_vars(self):
+        class MySchema(Schema):
+            spec = {'bar': IntConfigOption()}
+            foo = DictConfigOption(spec, strict=True)
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]\nbar=2\nbaz=3")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertRaises(ValueError, parser.parse_all)
+
+
+class TestLinesOfDictConfigOption(unittest.TestCase):
+    def test_parse_lines_of_dict(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(
+                        DictConfigOption({'bar': StringConfigOption(),
+                                          'baz': IntConfigOption(),
+                                          'bla': BoolConfigOption(),
+                                          }))
+        config = StringIO("""[__main__]
+foo = mylist0
+      mylist1
+[mylist0]
+bar=baz
+baz=42
+bla=Yes
+[mylist1]
+bar=zort
+baz=123
+bla=0
+""")
+        expected_values = {
+            '__main__': {'foo': [{'bar': 'baz', 'baz': 42, 'bla': True},
+                                {'bar': 'zort', 'baz': 123, 'bla': False},
+                               ]}}
+
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+
+class TestDictWithDicts(unittest.TestCase):
+    def test_parse_dict_with_dicts(self):
+        innerspec = {'bar': StringConfigOption(),
+                     'baz': IntConfigOption(),
+                     'bla': BoolConfigOption(),
+                    }
+        spec = {'name': StringConfigOption(),
+                'size': IntConfigOption(),
+                'options': DictConfigOption(innerspec)}
+        class MySchema(Schema):
+            foo = DictConfigOption(spec)
+        config = StringIO("""[__main__]
+foo = outerdict
+[outerdict]
+options = innerdict
+[innerdict]
+bar = something
+baz = 42
+""")
+        expected_values = {
+            '__main__': {'foo': {'name': '', 'size': 0,
+                                'options': {'bar': 'something', 'baz': 42,
+                                            'bla': False}}}}
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEqual(parser.values(), expected_values)
+
+
+class TestListOfTuples(unittest.TestCase):
+    def setUp(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(item=TupleConfigOption(length=3))
+        schema = MySchema()
+        self.parser = SchemaConfigParser(schema)
+
+    def test_parse_list_of_tuples(self):
+        config = StringIO('[__main__]\nfoo = a, b, c\n      d, e, f')
+        expected_values = {
+            '__main__': {'foo': [('a', 'b', 'c'), ('d', 'e', 'f')]}}
+        self.parser.readfp(config)
+        self.assertEqual(self.parser.values(), expected_values)
+
+    def test_parse_wrong_tuple_size(self):
+        config = StringIO('[__main__]\nfoo = a, b, c\n      d, e')
+        self.parser.readfp(config)
+        self.assertRaises(ValueError, self.parser.values)
+
+    def test_parse_empty_tuple(self):
+        config = StringIO('[__main__]\nfoo=()')
+        expected_values = {'__main__': {'foo': [()]}}
+        self.parser.readfp(config)
+        self.assertEqual(self.parser.values(), expected_values)
+
+
+if __name__ == '__main__':
+    unittest.main()

=== added file 'tests/pyschema/test_parser.py'
--- tests/pyschema/test_parser.py	1970-01-01 00:00:00 +0000
+++ tests/pyschema/test_parser.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,1009 @@
+# -*- coding: utf-8 -*-
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+import os
+import shutil
+import tempfile
+import unittest
+from StringIO import StringIO
+
+from ConfigParser import (InterpolationMissingOptionError,
+    InterpolationDepthError, NoSectionError)
+
+from configglue.pyschema import ConfigSection
+from configglue.pyschema.options import (BoolConfigOption, DictConfigOption,
+    IntConfigOption, StringConfigOption, LinesConfigOption, TupleConfigOption)
+from configglue.pyschema.schema import Schema
+from configglue.pyschema.parser import (NoOptionError, SchemaConfigParser,
+    SchemaValidationError, CONFIG_FILE_ENCODING)
+
+
+class TestIncludes(unittest.TestCase):
+    def setUp(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+        self.schema = MySchema()
+        fd, self.name = tempfile.mkstemp(suffix='.cfg')
+        os.write(fd, '[__main__]\nfoo=bar\n')
+        os.close(fd)
+
+    def tearDown(self):
+        os.remove(self.name)
+
+    def test_basic_include(self):
+        config = StringIO('[__main__]\nincludes=%s' % self.name)
+        parser = SchemaConfigParser(self.schema)
+        parser.readfp(config, 'my.cfg')
+        self.assertEquals({'__main__': {'foo': 'bar'}}, parser.values())
+
+    def test_locate(self):
+        config = StringIO("[__main__]\nincludes=%s" % self.name)
+        parser = SchemaConfigParser(self.schema)
+        parser.readfp(config, 'my.cfg')
+
+        location = parser.locate(option='foo')
+        expected_location = self.name
+        self.assertEqual(expected_location, location)
+
+    def test_read_ioerror(self):
+        def mock_open(filename, mode='r', encoding='ascii'):
+            raise IOError
+        _open = __builtins__['open']
+        __builtins__['open'] = mock_open
+
+        parser = SchemaConfigParser(self.schema)
+        read_ok = parser.read(self.name)
+        self.assertEqual(read_ok, [])
+
+        __builtins__['open'] = _open
+
+    def test_relative_include(self):
+        def setup_config():
+            folder = tempfile.mkdtemp()
+
+            f = open("%s/first.cfg" % folder, 'w')
+            f.write("[__main__]\nfoo=1\nincludes=second.cfg")
+            f.close()
+
+            f = open("%s/second.cfg" % folder, 'w')
+            f.write("[__main__]\nbar=2\nincludes=sub/third.cfg")
+            f.close()
+
+            os.mkdir("%s/sub" % folder)
+            f = open("%s/sub/third.cfg" % folder, 'w')
+            f.write("[__main__]\nincludes=../fourth.cfg")
+            f.close()
+
+            f = open("%s/fourth.cfg" % folder, 'w')
+            f.write("[__main__]\nbaz=3")
+            f.close()
+
+            config = StringIO("[__main__]\nincludes=%s/first.cfg" % folder)
+            return config, folder
+
+        class MySchema(Schema):
+            foo = IntConfigOption()
+            bar = IntConfigOption()
+            baz = IntConfigOption()
+
+        config, folder = setup_config()
+        expected_values = {'__main__': {'foo': 1, 'bar': 2, 'baz': 3}}
+        parser = SchemaConfigParser(MySchema())
+        # make sure we start on a clean basedir
+        self.assertEqual(parser._basedir, '')
+        parser.readfp(config, 'my.cfg')
+        self.assertEqual(parser.values(), expected_values)
+        # make sure we leave the basedir clean
+        self.assertEqual(parser._basedir, '')
+
+        # silently remove any created files
+        try:
+            shutil.rmtree(folder)
+        except:
+            pass
+
+    def test_local_override(self):
+        def setup_config():
+            folder = tempfile.mkdtemp()
+
+            f = open("%s/first.cfg" % folder, 'w')
+            f.write("[__main__]\nfoo=1\nbar=2\nincludes=second.cfg")
+            f.close()
+
+            f = open("%s/second.cfg" % folder, 'w')
+            f.write("[__main__]\nbaz=3")
+            f.close()
+
+            config = StringIO("[__main__]\nfoo=4\nincludes=%s/first.cfg" % folder)
+            return config, folder
+
+        class MySchema(Schema):
+            foo = IntConfigOption()
+            bar = IntConfigOption()
+            baz = IntConfigOption()
+
+        config, folder = setup_config()
+        expected_values = {'__main__': {'foo': 4, 'bar': 2, 'baz': 3}}
+        parser = SchemaConfigParser(MySchema())
+        # make sure we start on a clean basedir
+        self.assertEqual(parser._basedir, '')
+        parser.readfp(config, 'my.cfg')
+        self.assertEqual(parser.values(), expected_values)
+        # make sure we leave the basedir clean
+        self.assertEqual(parser._basedir, '')
+
+        # silently remove any created files
+        try:
+            shutil.rmtree(folder)
+        except:
+            pass
+
+
+class TestInterpolation(unittest.TestCase):
+    def test_basic_interpolate(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+            bar = BoolConfigOption()
+        config = StringIO('[__main__]\nbar=%(foo)s\nfoo=True')
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config, 'my.cfg')
+        self.assertEquals({'__main__': {'foo': 'True', 'bar': True}},
+                          parser.values())
+
+    def test_interpolate_missing_option(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+            bar = BoolConfigOption()
+
+        section = '__main__'
+        option = 'foo'
+        rawval = '%(baz)s'
+        vars = {}
+        parser = SchemaConfigParser(MySchema())
+        self.assertRaises(InterpolationMissingOptionError,
+            parser._interpolate, section, option, rawval, vars)
+
+    def test_interpolate_too_deep(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+            bar = BoolConfigOption()
+
+        section = '__main__'
+        option = 'foo'
+        rawval = '%(bar)s'
+        vars = {'foo': '%(bar)s', 'bar': '%(foo)s'}
+        parser = SchemaConfigParser(MySchema())
+        self.assertRaises(InterpolationDepthError,
+            parser._interpolate, section, option, rawval, vars)
+
+    def test_interpolate_incomplete_format(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+            bar = BoolConfigOption()
+
+        section = '__main__'
+        option = 'foo'
+        rawval = '%(bar)'
+        vars = {'foo': '%(bar)s', 'bar': 'pepe'}
+        parser = SchemaConfigParser(MySchema())
+        self.assertRaises(ValueError, parser._interpolate, section, option,
+                          rawval, vars)
+
+    def test_interpolate_across_sections(self):
+        class MySchema(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+
+            baz = ConfigSection()
+            baz.wham = IntConfigOption()
+
+        config = StringIO("[foo]\nbar=%(wham)s\n[baz]\nwham=42")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertRaises(InterpolationMissingOptionError,
+            parser.get, 'foo', 'bar')
+
+    def test_interpolate_invalid_key(self):
+        class MySchema(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+
+            baz = ConfigSection()
+            baz.wham = IntConfigOption()
+
+        config = StringIO("[foo]\nbar=%(wham)\n[baz]\nwham=42")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertRaises(InterpolationMissingOptionError, parser.get,
+                          'foo', 'bar')
+
+    def test_get_interpolation_keys_string(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+        config = StringIO("[__main__]\nfoo=%(bar)s")
+        expected = ('%(bar)s', set(['bar']))
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        result = parser._get_interpolation_keys('__main__', 'foo')
+        self.assertEqual(result, expected)
+
+    def test_get_interpolation_keys_int(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+        config = StringIO("[__main__]\nfoo=%(bar)s")
+        expected = ('%(bar)s', set(['bar']))
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        result = parser._get_interpolation_keys('__main__', 'foo')
+        self.assertEqual(result, expected)
+
+    def test_get_interpolation_keys_bool(self):
+        class MySchema(Schema):
+            foo = BoolConfigOption()
+        config = StringIO("[__main__]\nfoo=%(bar)s")
+        expected = ('%(bar)s', set(['bar']))
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        result = parser._get_interpolation_keys('__main__', 'foo')
+        self.assertEqual(result, expected)
+
+    def test_get_interpolation_keys_tuple(self):
+        class MySchema(Schema):
+            foo = TupleConfigOption(2)
+        config = StringIO("[__main__]\nfoo=%(bar)s,%(baz)s")
+        expected = ('%(bar)s,%(baz)s', set(['bar', 'baz']))
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        result = parser._get_interpolation_keys('__main__', 'foo')
+        self.assertEqual(result, expected)
+
+    def test_get_interpolation_keys_lines(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(item=StringConfigOption())
+        config = StringIO("[__main__]\nfoo=%(bar)s\n    %(baz)s")
+        expected = ('%(bar)s\n%(baz)s', set(['bar', 'baz']))
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        result = parser._get_interpolation_keys('__main__', 'foo')
+        self.assertEqual(result, expected)
+
+    def test_get_interpolation_keys_tuple_lines(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(item=TupleConfigOption(2))
+        config = StringIO("[__main__]\nfoo=%(bar)s,%(bar)s\n    %(baz)s,%(baz)s")
+        expected = ('%(bar)s,%(bar)s\n%(baz)s,%(baz)s',
+                    set(['bar', 'baz']))
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        result = parser._get_interpolation_keys('__main__', 'foo')
+        self.assertEqual(result, expected)
+
+    def test_get_interpolation_keys_dict(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'a': IntConfigOption()})
+        config = StringIO("[__noschema__]\nbar=4\n[__main__]\nfoo=mydict\n[mydict]\na=%(bar)s")
+        expected = ('mydict', set([]))
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        result = parser._get_interpolation_keys('__main__', 'foo')
+        self.assertEqual(result, expected)
+
+    def test_interpolate_value_duplicate_key(self):
+        class MySchema(Schema):
+            foo = TupleConfigOption(2)
+        config = StringIO("[__noschema__]\nbar=4\n[__main__]\nfoo=%(bar)s,%(bar)s")
+        expected_value = '4,4'
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        value = parser._interpolate_value('__main__', 'foo')
+        self.assertEqual(value, expected_value)
+
+    def test_interpolate_value_invalid_key(self):
+        class MySchema(Schema):
+            foo = TupleConfigOption(2)
+        config = StringIO("[other]\nbar=4\n[__main__]\nfoo=%(bar)s,%(bar)s")
+        expected_value = None
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        value = parser._interpolate_value('__main__', 'foo')
+        self.assertEqual(value, expected_value)
+
+    def test_get_with_raw_value(self):
+        class MySchema(Schema):
+            foo = StringConfigOption(raw=True)
+        config = StringIO('[__main__]\nfoo=blah%(asd)##$@@dddf2kjhkjs')
+        expected_value = 'blah%(asd)##$@@dddf2kjhkjs'
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        value = parser.get('__main__', 'foo')
+        self.assertEqual(value, expected_value)
+
+    def test_interpolate_parse_dict(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'a': IntConfigOption()})
+        config = StringIO("[__noschema__]\nbar=4\n[__main__]\nfoo=mydict\n[mydict]\na=%(bar)s")
+        expected = {'__main__': {'foo': {'a': 4}}}
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        result = parser.values()
+        self.assertEqual(result, expected)
+
+
+class TestSchemaConfigParser(unittest.TestCase):
+    def setUp(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+        self.schema = MySchema()
+        self.parser = SchemaConfigParser(self.schema)
+        self.config = StringIO("[__main__]\nfoo = bar")
+
+    def test_init_no_args(self):
+        self.assertRaises(TypeError, SchemaConfigParser)
+
+    def test_init_valid_schema(self):
+        self.assertEqual(self.parser.schema, self.schema)
+
+    def test_init_invalid_schema(self):
+        class MyInvalidSchema(Schema):
+            __main__ = ConfigSection()
+
+        self.assertRaises(SchemaValidationError, SchemaConfigParser,
+                          MyInvalidSchema())
+
+    def test_items(self):
+        self.parser.readfp(self.config)
+        items = self.parser.items('__main__')
+        self.assertEqual(set(items), set([('foo', 'bar')]))
+
+    def test_items_no_section(self):
+        self.assertRaises(NoSectionError, self.parser.items, '__main__')
+
+    def test_items_raw(self):
+        config = StringIO('[__main__]\nfoo=%(baz)s')
+        self.parser.readfp(config)
+        items = self.parser.items('__main__', raw=True)
+        self.assertEqual(set(items), set([('foo', '%(baz)s')]))
+
+    def test_items_vars(self):
+        config = StringIO('[__main__]\nfoo=%(baz)s')
+        self.parser.readfp(config)
+        items = self.parser.items('__main__', vars={'baz': '42'})
+        self.assertEqual(set(items), set([('foo', '42'), ('baz', '42')]))
+
+    def test_items_interpolate(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+            baz = ConfigSection()
+            baz.bar = StringConfigOption()
+
+        parser = SchemaConfigParser(MySchema())
+        config = StringIO('[__main__]\nfoo=%(bar)s\n[baz]\nbar=42')
+        parser.readfp(config)
+        # test interpolate
+        items = parser.items('baz')
+        self.assertEqual(items, {'bar': '42'}.items())
+
+    def test_items_interpolate_error(self):
+        config = StringIO('[__main__]\nfoo=%(bar)s')
+        self.parser.readfp(config)
+        self.assertRaises(InterpolationMissingOptionError, self.parser.items,
+                          '__main__')
+
+    def test_values_empty_parser(self):
+        values = self.parser.values()
+        self.assertEqual(values, {'__main__': {'foo': ''}})
+
+    def test_values_full_parser(self):
+        expected_values = {'__main__': {'foo': 'bar'}}
+
+        self.parser.readfp(self.config)
+        values = self.parser.values()
+        self.assertEqual(expected_values, values)
+        values = self.parser.values(section='__main__')
+        self.assertEqual(expected_values['__main__'], values)
+
+    def test_values_many_sections_same_option(self):
+        class MySchema(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+
+            baz = ConfigSection()
+            baz.bar = IntConfigOption()
+
+        config = StringIO("[foo]\nbar=3\n[baz]\nbar=4")
+        expected_values = {'foo': {'bar': 3}, 'baz': {'bar': 4}}
+
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        values = parser.values()
+        self.assertEqual(values, expected_values)
+
+    def test_values_many_sections_different_options(self):
+        class MySchema(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+
+            bar = ConfigSection()
+            bar.baz = IntConfigOption()
+
+        config = StringIO("[foo]\nbar=3\n[bar]\nbaz=4")
+        expected_values = {'foo': {'bar': 3}, 'bar': {'baz': 4}}
+
+        schema = MySchema()
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        values = parser.values()
+        self.assertEqual(values, expected_values)
+
+    def test_parse_option(self):
+        class MyOtherSchema(Schema):
+            foo = ConfigSection()
+            foo.bar = StringConfigOption()
+
+        expected_value = 'baz'
+        config = StringIO("[foo]\nbar = baz")
+        parser = SchemaConfigParser(MyOtherSchema())
+        parser.readfp(config)
+        value = parser.get('foo', 'bar')
+        self.assertEqual(value, expected_value)
+
+    def test_parse_invalid_section(self):
+        self.assertRaises(NoSectionError, self.parser.parse, 'bar', 'baz', '1')
+
+    def test_default_values(self):
+        class MySchema(Schema):
+            foo = BoolConfigOption(default=True)
+            bar = ConfigSection()
+            bar.baz = IntConfigOption()
+            bar.bla = StringConfigOption(default='hello')
+        schema = MySchema()
+        config = StringIO("[bar]\nbaz=123")
+        expected_values = {'__main__': {'foo': True},
+                           'bar': {'baz': 123, 'bla': 'hello'}}
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEquals(expected_values, parser.values())
+
+        config = StringIO("[bar]\nbla=123")
+        expected = {'__main__': {'foo': True}, 'bar': {'baz': 0, 'bla': '123'}}
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEquals(expected, parser.values())
+
+    def test_fatal_options(self):
+        class MySchema(Schema):
+            foo = IntConfigOption(fatal=True)
+            bar = IntConfigOption()
+        schema = MySchema()
+        config = StringIO("[__main__]\nfoo=123")
+        expected = {'__main__': {'foo': 123, 'bar': 0}}
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertEquals(expected, parser.values())
+
+        config = StringIO("[__main__]\nbar=123")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+        self.assertRaises(NoOptionError, parser.values)
+
+    def test_extra_sections(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': IntConfigOption()})
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]\nbar=1")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        expected_sections = set(['mydict'])
+        extra_sections = parser.extra_sections
+        self.assertEqual(expected_sections, extra_sections)
+
+    def test_multiple_extra_sections(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(
+                item=DictConfigOption({'bar': IntConfigOption()}))
+
+        config = StringIO('[__main__]\nfoo=d1\n    d2\n    d3\n'
+                          '[d1]\nbar=1\n[d2]\nbar=2\n[d3]\nbar=3')
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        expected_sections = set(['d1', 'd2', 'd3'])
+        extra_sections = parser.extra_sections
+        self.assertEqual(expected_sections, extra_sections)
+
+    def test_get_default(self):
+        config = StringIO("[__main__]\n")
+        expected = ''
+        self.parser.readfp(config)
+        default = self.parser._get_default('__main__', 'foo')
+        self.assertEqual(default, expected)
+
+    def test_get_default_noschema(self):
+        config = StringIO("[__noschema__]\nbar=1\n[__main__]\n")
+        expected = '1'
+        self.parser.readfp(config)
+        default = self.parser._get_default('__noschema__', 'bar')
+        self.assertEqual(default, expected)
+
+    def test_get_default_from_section(self):
+        class MySchema(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+        config = StringIO("[__main__]\n")
+        expected = '0'
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        default = parser._get_default('foo', 'bar')
+        self.assertEqual(default, expected)
+
+    def test_get_default_no_option(self):
+        expected = None
+        default = self.parser._get_default('__main__', 'bar')
+        self.assertEqual(default, expected)
+
+    def test_get_default_no_section(self):
+        expected = None
+        default = self.parser._get_default('foo', 'bar')
+        self.assertEqual(default, expected)
+
+    def test_multi_file_dict_config(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': IntConfigOption(),
+                                    'baz': IntConfigOption()},
+                                   strict=True)
+        config1 = StringIO('[__main__]\nfoo=mydict\n[mydict]\nbar=1\nbaz=1')
+        config2 = StringIO('[mydict]\nbaz=2')
+        expected_values = {'__main__': {'foo': {'bar': 1, 'baz': 2}}}
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config1)
+        parser.readfp(config2)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_multi_file_dict_list_config(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(
+                item=DictConfigOption({'bar': IntConfigOption(),
+                                       'baz': IntConfigOption()},
+                                      strict=True))
+
+        config1 = StringIO('[__main__]\nfoo=mydict\n[mydict]\nbar=1\nbaz=1')
+        expected_values = {'__main__': {'foo': [{'bar': 1, 'baz': 1}]}}
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config1)
+        self.assertEqual(parser.values(), expected_values)
+
+        # override used dictionaries
+        config2 = StringIO('[__main__]\nfoo=otherdict\n[otherdict]\nbar=2')
+        expected_values = {'__main__': {'foo': [{'bar': 2, 'baz': 0}]}}
+        parser.readfp(config2)
+        self.assertEqual(parser.values(), expected_values)
+
+        # override existing dictionaries
+        config3 = StringIO('[otherdict]\nbaz=3')
+        expected_values = {'__main__': {'foo': [{'bar': 2, 'baz': 3}]}}
+        parser.readfp(config3)
+        self.assertEqual(parser.values(), expected_values)
+
+        # reuse existing dict
+        config4 = StringIO('[__main__]\nfoo=mydict\n    otherdict')
+        expected_values = {'__main__': {'foo': [{'bar': 1, 'baz': 1},
+                                               {'bar': 2, 'baz': 3}]}}
+        parser.readfp(config4)
+        self.assertEqual(parser.values(), expected_values)
+
+    def test_read_multiple_files(self):
+        def setup_config():
+            folder = tempfile.mkdtemp()
+
+            f = open("%s/first.cfg" % folder, 'w')
+            f.write("[__main__]\nfoo=foo")
+            f.close()
+
+            f = open("%s/second.cfg" % folder, 'w')
+            f.write("[__main__]\nfoo=bar")
+            f.close()
+
+            files = ["%s/first.cfg" % folder, "%s/second.cfg" % folder]
+            return files, folder
+
+        files, folder = setup_config()
+        self.parser.read(files)
+        self.assertEqual(self.parser.values(), {'__main__': {'foo': 'bar'}})
+
+        # silently remove any created files
+        try:
+            shutil.rmtree(folder)
+        except:
+            pass
+
+    def test_read_utf8_encoded_file(self):
+        # create config file
+        fp, filename = tempfile.mkstemp()
+
+        try:
+            f = open(filename, 'w')
+            f.write(u'[__main__]\nfoo=€'.encode(CONFIG_FILE_ENCODING))
+            f.close()
+
+            self.parser.read(filename)
+            self.assertEqual(self.parser.values(), {'__main__': {'foo': u'€'}})
+        finally:
+            # destroy config file
+            os.remove(filename)
+
+    def test_readfp_with_utf8_encoded_text(self):
+        config = StringIO(u'[__main__]\nfoo=€'.encode(CONFIG_FILE_ENCODING))
+        self.parser.readfp(config)
+        self.assertEqual(self.parser.values(), {'__main__': {'foo': u'€'}})
+
+    def test_set(self):
+        with tempfile.NamedTemporaryFile() as f:
+            f.write('[__main__]\nfoo=1')
+            f.flush()
+
+            self.parser.read(f.name)
+            self.assertEqual(self.parser._dirty, {})
+            self.assertEqual(self.parser.get('__main__', 'foo'), '1')
+            self.parser.set('__main__', 'foo', '2')
+            self.assertEqual(self.parser.get('__main__', 'foo'), '2')
+            self.assertEqual(self.parser._dirty,
+                {f.name: {'__main__': {'foo': '2'}}})
+
+    def test_save_config(self):
+        expected_output = u'[__main__]\nfoo = 42\n\n'
+        config = StringIO(u'[__main__]\nfoo=42')
+        self.parser.readfp(config)
+
+        # create config file
+        fp, filename = tempfile.mkstemp()
+        self.parser.save(open(filename, 'w'))
+        self.assertEqual(open(filename, 'r').read(), expected_output)
+
+        self.parser.save(filename)
+        self.assertEqual(open(filename, 'r').read(), expected_output)
+
+        # remove the file
+        os.unlink(filename)
+
+    def test_save_config_same_files(self):
+        def setup_config():
+            folder = tempfile.mkdtemp()
+
+            f = open("%s/first.cfg" % folder, 'w')
+            f.write("[__main__]\nfoo=1")
+            f.close()
+
+            f = open("%s/second.cfg" % folder, 'w')
+            f.write("[__main__]\nbar=2")
+            f.close()
+
+            files = ["%s/first.cfg" % folder, "%s/second.cfg" % folder]
+            return files, folder
+
+        class MySchema(Schema):
+            foo = StringConfigOption()
+            bar = StringConfigOption()
+
+        self.parser = SchemaConfigParser(MySchema())
+
+        files, folder = setup_config()
+        self.parser.read(files)
+        self.parser.set('__main__', 'foo', '42')
+        self.parser.set('__main__', 'bar', '42')
+        self.parser.save()
+
+        # test the changes were correctly saved
+        data = open("%s/first.cfg" % folder).read()
+        self.assertTrue('foo = 42' in data)
+        self.assertFalse('bar = 42' in data)
+        data = open("%s/second.cfg" % folder).read()
+        self.assertFalse('foo = 42' in data)
+        self.assertTrue('bar = 42' in data)
+
+        # silently remove any created files
+        try:
+            shutil.rmtree(folder)
+        except:
+            pass
+
+
+class TestParserIsValid(unittest.TestCase):
+    def setUp(self):
+        class MySchema(Schema):
+            foo = StringConfigOption()
+        self.schema = MySchema()
+        self.parser = SchemaConfigParser(self.schema)
+        self.config = StringIO("[__main__]\nfoo = bar")
+
+    def test_basic_is_valid(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+
+        schema = MySchema()
+        config = StringIO("[__main__]\nfoo = 5")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+
+        self.assertTrue(parser.is_valid())
+
+    def test_basic_is_valid_with_report(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+
+        config = StringIO("[__main__]\nfoo=5")
+        expected = (True, [])
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        valid, errors = parser.is_valid(report=True)
+        self.assertEqual((valid, errors), expected)
+
+    def test_basic_is_not_valid(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+
+        schema = MySchema()
+        config = StringIO("[__main__]\nfoo = 5\nbar = 6")
+        parser = SchemaConfigParser(schema)
+        parser.readfp(config)
+
+        self.assertFalse(parser.is_valid())
+
+    def test_basic_is_not_valid_with_report(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+
+        config = StringIO("[__main__]\nfoo=5\nbar=6")
+        errors = ["Configuration includes invalid options for section '__main__': bar"]
+        expected = (False, errors)
+
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        valid, errors = parser.is_valid(report=True)
+        self.assertEqual((valid, errors), expected)
+
+    def test_is_not_valid_parser_error(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+
+        def mock_parse_all(self):
+            assert False
+
+        schema = MySchema()
+        config = StringIO("[__main__]\nfoo = 5")
+        parser = SchemaConfigParser(schema)
+        parser.parse_all = mock_parse_all
+        parser.readfp(config)
+
+        self.assertFalse(parser.is_valid())
+
+    def test_parse_invalid_section(self):
+        config = StringIO("[bar]\nbaz=foo")
+        self.parser.readfp(config)
+
+        self.assertFalse(self.parser.is_valid())
+
+    def test_different_sections(self):
+        config = StringIO("[__main__]\nfoo=1\n[bar]\nbaz=2")
+        self.parser.readfp(config)
+
+        self.assertFalse(self.parser.is_valid())
+
+    def test_missing_fatal_options(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+            bar = IntConfigOption(fatal=True)
+
+        config = StringIO("[__main__]\nfoo=1")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+
+        self.assertFalse(parser.is_valid())
+
+    def test_missing_nonfatal_options(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+            bar = IntConfigOption(fatal=True)
+
+        config = StringIO("[__main__]\nbar=2")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+
+        self.assertTrue(parser.is_valid())
+
+    def test_extra_sections(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': IntConfigOption()})
+
+        config = StringIO("[__main__]\nfoo=mydict\n[mydict]\nbar=1")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        self.assertTrue(parser.is_valid())
+
+    def test_extra_sections_when_dict_with_nested_dicts(self):
+        class MySchema(Schema):
+            foo = DictConfigOption(item=DictConfigOption())
+
+        config = StringIO("""
+[__main__]
+foo=dict1
+[dict1]
+bar=dict2
+[dict2]
+baz=42
+""")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        self.assertEqual(parser.values(),
+            {'__main__': {'foo': {'bar': {'baz': '42'}}}})
+        self.assertTrue(parser.is_valid())
+
+    def test_extra_sections_with_nested_dicts_strict(self):
+        class MySchema(Schema):
+            foo = DictConfigOption({'bar': DictConfigOption()}, strict=True)
+
+        config = StringIO("""
+[__main__]
+foo=dict1
+[dict1]
+bar=dict2
+[dict2]
+baz=42
+""")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        self.assertEqual(parser.values(),
+            {'__main__': {'foo': {'bar': {'baz': '42'}}}})
+        self.assertTrue(parser.is_valid())
+
+    def test_extra_sections_when_lines_dict_with_nested_dicts(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(
+                item=DictConfigOption(item=DictConfigOption()))
+
+        config = StringIO("""
+[__main__]
+foo = dict1
+      dict2
+[dict1]
+bar = dict3
+[dict2]
+baz = dict4
+[dict3]
+wham = 1
+[dict4]
+whaz = 2
+""")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        self.assertEqual(parser.values(),
+            {'__main__': {'foo': [
+                {'bar': {'wham': '1'}},
+                {'baz': {'whaz': '2'}}
+            ]}})
+        self.assertTrue(parser.is_valid())
+
+    def test_extra_sections_when_dict_with_nested_lines_dicts(self):
+        class MySchema(Schema):
+            foo = DictConfigOption(
+                item=LinesConfigOption(item=DictConfigOption()))
+
+        config = StringIO("""
+[__main__]
+foo = dict1
+[dict1]
+bar = dict2
+      dict3
+[dict2]
+baz = 1
+[dict3]
+wham = 2
+""")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        self.assertEqual(parser.values(),
+            {'__main__': {'foo': {'bar': [{'baz': '1'}, {'wham': '2'}]}}})
+        self.assertTrue(parser.is_valid())
+
+    def test_extra_sections_when_lines_dict_with_nested_lines_dicts(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(
+                item=DictConfigOption(
+                    item=LinesConfigOption(item=DictConfigOption())))
+
+        config = StringIO("""
+[__main__]
+foo = dict1
+      dict2
+[dict1]
+bar = dict3
+      dict4
+[dict2]
+baz = dict5
+      dict6
+[dict3]
+wham = 1
+[dict4]
+whaz = 2
+[dict5]
+whoosh = 3
+[dict6]
+swoosh = 4
+""")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        self.assertEqual(parser.values(),
+            {'__main__': {'foo': [
+                {'bar': [{'wham': '1'}, {'whaz': '2'}]},
+                {'baz': [{'whoosh': '3'}, {'swoosh': '4'}]}
+            ]}})
+        self.assertTrue(parser.is_valid())
+
+    def test_multiple_extra_sections(self):
+        class MySchema(Schema):
+            foo = LinesConfigOption(
+                item=DictConfigOption({'bar': IntConfigOption()}))
+
+        config = StringIO('[__main__]\nfoo=d1\n    d2\n    d3\n'
+                          '[d1]\nbar=1\n[d2]\nbar=2\n[d3]\nbar=3')
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        parser.parse_all()
+
+        self.assertTrue(parser.is_valid())
+
+    def test_noschema_section(self):
+        config = StringIO("[__main__]\nfoo=%(bar)s\n[__noschema__]\nbar=hello")
+        parser = SchemaConfigParser(self.schema)
+        parser.readfp(config)
+        parser.parse_all()
+
+        self.assertTrue(parser.is_valid())
+
+
+if __name__ == '__main__':
+    unittest.main()

=== added file 'tests/pyschema/test_schema.py'
--- tests/pyschema/test_schema.py	1970-01-01 00:00:00 +0000
+++ tests/pyschema/test_schema.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,136 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+import unittest
+
+from configglue.pyschema import ConfigSection
+from configglue.pyschema.options import (BoolConfigOption,
+    IntConfigOption, LinesConfigOption)
+from configglue.pyschema.schema import Schema
+
+
+class TestSchema(unittest.TestCase):
+    def test_sections(self):
+        class MySchema(Schema):
+            foo = BoolConfigOption()
+
+        class MyOtherSchema(Schema):
+            web = ConfigSection()
+            web.bar = IntConfigOption()
+            froo = ConfigSection()
+            froo.twaddle = LinesConfigOption(item=BoolConfigOption())
+
+        class MyThirdSchema(Schema):
+            bar = IntConfigOption()
+            froo = ConfigSection()
+            froo.twaddle = LinesConfigOption(item=BoolConfigOption())
+
+        schema = MySchema()
+        names = set(s.name for s in schema.sections())
+        self.assertEquals(set(['__main__']), names)
+
+        schema = MyOtherSchema()
+        names = set(s.name for s in schema.sections())
+        self.assertEquals(set(['web', 'froo']), names)
+
+        schema = MyThirdSchema()
+        names = set(s.name for s in schema.sections())
+        self.assertEquals(set(['__main__', 'froo']), names)
+
+    def test_schema_validation(self):
+        class BorkenSchema(Schema):
+            __main__ = ConfigSection()
+            __main__.foo = BoolConfigOption()
+
+        class SomeSchema(Schema):
+            mysection = ConfigSection()
+
+        schema = BorkenSchema()
+        self.assertFalse(schema.is_valid())
+
+        schema = SomeSchema()
+        self.assertTrue(schema.is_valid())
+
+    def test_names(self):
+        class MySchema(Schema):
+            foo = BoolConfigOption()
+            bar = ConfigSection()
+            bar.baz = IntConfigOption()
+
+        schema = MySchema()
+        self.assertEquals('foo', schema.foo.name)
+        self.assertEquals('__main__', schema.foo.section.name)
+        self.assertEquals('bar', schema.bar.name)
+        self.assertEquals('baz', schema.bar.baz.name)
+        self.assertEquals('bar', schema.bar.baz.section.name)
+
+    def test_options(self):
+        class MySchema(Schema):
+            foo = BoolConfigOption()
+            bar = ConfigSection()
+            bar.baz = IntConfigOption()
+
+        schema = MySchema()
+        names = set(s.name for s in schema.options())
+        self.assertEquals(set(['foo', 'baz']), names)
+        names = set(s.name for s in schema.options('__main__'))
+        self.assertEquals(set(['foo']), names)
+        names = set(s.name for s in schema.options('bar'))
+        self.assertEquals(set(['baz']), names)
+
+    def test_include(self):
+        schema = Schema()
+        self.assertTrue(hasattr(schema, 'includes'))
+
+    def test_equal(self):
+        class MySchema(Schema):
+            foo = IntConfigOption()
+        class OtherSchema(Schema):
+            bar = IntConfigOption()
+
+        self.assertEqual(MySchema(), MySchema())
+        self.assertNotEqual(MySchema(), OtherSchema())
+
+
+class TestSchemaInheritance(unittest.TestCase):
+    def setUp(self):
+        class SchemaA(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+        class SchemaB(SchemaA):
+            baz = ConfigSection()
+            baz.wham = IntConfigOption()
+
+        self.schema = SchemaB()
+
+    def test_basic_inheritance(self):
+        names = [('foo', ['bar']), ('baz', ['wham'])]
+        for section, options in names:
+            section_obj = getattr(self.schema, section)
+            self.assertTrue(isinstance(section_obj, ConfigSection))
+            for option in options:
+                option_obj = getattr(section_obj, option)
+                self.assertTrue(isinstance(option_obj, IntConfigOption))
+
+    def test_inherited_sections(self):
+        names = set(s.name for s in self.schema.sections())
+        self.assertEqual(set(['foo', 'baz']), names)
+
+    def test_inherited_options(self):
+        names = set(s.name for s in self.schema.options())
+        self.assertEqual(set(['bar', 'wham']), names)
+

=== added file 'tests/pyschema/test_schemaconfig.py'
--- tests/pyschema/test_schemaconfig.py	1970-01-01 00:00:00 +0000
+++ tests/pyschema/test_schemaconfig.py	2010-08-04 21:21:41 +0000
@@ -0,0 +1,181 @@
+###############################################################################
+# 
+# configglue -- glue for your apps' configuration
+# 
+# A library for simple, DRY configuration of applications
+# 
+# (C) 2009--2010 by Canonical Ltd.
+# originally by John R. Lenton <john.lenton@xxxxxxxxxxxxx>
+# incorporating schemaconfig as configglue.pyschema
+# schemaconfig originally by Ricardo Kirkner <ricardo.kirkner@xxxxxxxxxxxxx>
+# 
+# Released under the BSD License (see the file LICENSE)
+# 
+# For bug reports, support, and new releases: http://launchpad.net/configglue
+# 
+###############################################################################
+
+import unittest
+import sys
+from StringIO import StringIO
+
+from configglue.pyschema import ConfigOption, ConfigSection, schemaconfigglue
+from configglue.pyschema.options import IntConfigOption
+from configglue.pyschema.parser import SchemaConfigParser
+from configglue.pyschema.schema import Schema
+
+
+class TestConfigOption(unittest.TestCase):
+    def test_repr_name(self):
+        opt = ConfigOption()
+        expected = "<ConfigOption>"
+        self.assertEqual(repr(opt), expected)
+
+        opt = ConfigOption(name='name')
+        expected = "<ConfigOption name>"
+        self.assertEqual(repr(opt), expected)
+
+        sect = ConfigSection(name='sect')
+        opt = ConfigOption(name='name', section=sect)
+        expected = "<ConfigOption sect.name>"
+        self.assertEqual(repr(opt), expected)
+
+    def test_repr_extra(self):
+        opt = ConfigOption(name='name', raw=True)
+        expected = "<ConfigOption name raw>"
+        self.assertEqual(repr(opt), expected)
+
+        opt = ConfigOption(name='name', fatal=True)
+        expected = "<ConfigOption name fatal>"
+        self.assertEqual(repr(opt), expected)
+
+        opt = ConfigOption(name='name', raw=True, fatal=True)
+        expected = "<ConfigOption name raw fatal>"
+        self.assertEqual(repr(opt), expected)
+
+    def test_parse(self):
+        opt = ConfigOption()
+        self.assertRaises(NotImplementedError, opt.parse, '')
+
+    def test_equal(self):
+        opt1 = ConfigOption()
+        opt2 = ConfigOption(name='name', raw=True)
+
+        self.assertEqual(opt1, ConfigOption())
+        self.assertEqual(opt2, ConfigOption(name='name', raw=True))
+        self.assertNotEqual(opt1, opt2)
+        self.assertNotEqual(opt1, None)
+
+
+class TestConfigSection(unittest.TestCase):
+    def test_repr_name(self):
+        sect = ConfigSection()
+        expected = "<ConfigSection>"
+        self.assertEqual(repr(sect), expected)
+
+        sect = ConfigSection(name='sect')
+        expected = "<ConfigSection sect>"
+        self.assertEqual(repr(sect), expected)
+
+    def test_equal(self):
+        sec1 = ConfigSection()
+        sec2 = ConfigSection(name='sec2')
+
+        self.assertEqual(sec1, ConfigSection())
+        self.assertEqual(sec2, ConfigSection(name='sec2'))
+        self.assertNotEqual(sec1, sec2)
+
+    def test_has_option(self):
+        sec1 = ConfigSection()
+        sec1.foo = IntConfigOption()
+
+        self.assertTrue(sec1.has_option('foo'))
+        self.assertFalse(sec1.has_option('bar'))
+
+
+class TestSchemaConfigGlue(unittest.TestCase):
+    def setUp(self):
+        class MySchema(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+
+            baz = IntConfigOption(help='The baz option')
+
+        self.parser = SchemaConfigParser(MySchema())
+
+    def test_glue_no_op(self):
+        config = StringIO("[__main__]\nbaz=1")
+        self.parser.readfp(config)
+        self.assertEqual(self.parser.values(),
+            {'foo': {'bar': 0}, '__main__': {'baz': 1}})
+
+        op, options, args = schemaconfigglue(self.parser, argv=['--baz', '2'])
+        self.assertEqual(self.parser.values(),
+            {'foo': {'bar': 0}, '__main__': {'baz': 2}})
+
+    def test_glue_no_argv(self):
+        config = StringIO("[__main__]\nbaz=1")
+        self.parser.readfp(config)
+        self.assertEqual(self.parser.values(),
+            {'foo': {'bar': 0}, '__main__': {'baz': 1}})
+
+        _argv = sys.argv
+        sys.argv = []
+
+        op, options, args = schemaconfigglue(self.parser)
+        self.assertEqual(self.parser.values(),
+            {'foo': {'bar': 0}, '__main__': {'baz': 1}})
+
+        sys.argv = _argv
+
+    def test_glue_section_option(self):
+        config = StringIO("[foo]\nbar=1")
+        self.parser.readfp(config)
+        self.assertEqual(self.parser.values(),
+            {'foo': {'bar': 1}, '__main__': {'baz': 0}})
+
+        op, options, args = schemaconfigglue(self.parser,
+                                             argv=['--foo_bar', '2'])
+        self.assertEqual(self.parser.values(),
+                         {'foo': {'bar': 2}, '__main__': {'baz': 0}})
+
+    def test_ambiguous_option(self):
+        class MySchema(Schema):
+            foo = ConfigSection()
+            foo.baz = IntConfigOption()
+
+            bar = ConfigSection()
+            bar.baz = IntConfigOption()
+
+        config = StringIO("[foo]\nbaz=1")
+        parser = SchemaConfigParser(MySchema())
+        parser.readfp(config)
+        self.assertEqual(parser.values('foo'), {'baz': 1})
+        self.assertEqual(parser.values('bar'), {'baz': 0})
+
+        op, options, args = schemaconfigglue(
+            parser, argv=['--bar_baz', '2'])
+        self.assertEqual(parser.values('foo'), {'baz': 1})
+        self.assertEqual(parser.values('bar'), {'baz': 2})
+
+    def test_help(self):
+        config = StringIO("[foo]\nbar=1")
+        self.parser.readfp(config)
+        self.assertEqual(self.parser.values(),
+            {'foo': {'bar': 1}, '__main__': {'baz': 0}})
+
+        # replace stdout to capture its value
+        stdout = StringIO()
+        _stdout = sys.stdout
+        sys.stdout = stdout
+        # call the method and assert its value
+        self.assertRaises(SystemExit, schemaconfigglue, self.parser,
+            argv=['--help'])
+        # replace stdout again to cleanup
+        sys.stdout = _stdout
+
+        # assert the value of stdout is correct
+        stdout.seek(0)
+        output = stdout.read()
+        self.assertTrue(output.startswith('Usage:'))
+