← Back to team overview

configglue team mailing list archive

[Merge] lp:~ricardokirkner/configglue/django-configglue-inspired-fixes into lp:configglue

 

Ricardo Kirkner has proposed merging lp:~ricardokirkner/configglue/django-configglue-inspired-fixes into lp:configglue.

Requested reviews:
  Configglue developers (configglue)

For more details, see:
https://code.launchpad.net/~ricardokirkner/configglue/django-configglue-inspired-fixes/+merge/65594

Overview
========

Fixes to issues raised while working on getting django-configglue compatible with the latest configglue (revno 59)

Details
=======

While updating configglue to work with the latest configglue, some issues were detected. Those issue are fixed in this branch so that django-configglue can be updated to work with the latest configglue version.
-- 
https://code.launchpad.net/~ricardokirkner/configglue/django-configglue-inspired-fixes/+merge/65594
Your team Configglue developers is requested to review the proposed merge of lp:~ricardokirkner/configglue/django-configglue-inspired-fixes into lp:configglue.
=== modified file 'configglue/app/tests/test_base.py'
--- configglue/app/tests/test_base.py	2011-06-18 15:48:15 +0000
+++ configglue/app/tests/test_base.py	2011-06-22 23:11:36 +0000
@@ -1,5 +1,5 @@
 import user
-from ConfigParser import NoSectionError
+from ConfigParser import NoOptionError
 from unittest import TestCase
 
 from mock import (
@@ -64,7 +64,7 @@
     def test_glue_invalid_config(self):
         class MySchema(Schema):
             foo = IntOption(fatal=True)
-        self.assertRaises(NoSectionError, make_app, schema=MySchema)
+        self.assertRaises(SystemExit, make_app, schema=MySchema)
 
     def test_get_config_files(self):
         app = make_app()

=== modified file 'configglue/pyschema/glue.py'
--- configglue/pyschema/glue.py	2011-06-18 20:20:34 +0000
+++ configglue/pyschema/glue.py	2011-06-22 23:11:36 +0000
@@ -17,6 +17,10 @@
 
 import os
 import sys
+from ConfigParser import (
+    NoOptionError,
+    NoSectionError,
+)
 from optparse import OptionParser
 from collections import namedtuple
 
@@ -63,7 +67,8 @@
             kwargs = {}
             if option.help:
                 kwargs['help'] = option.help
-            kwargs['default'] = parser.get(section.name, option.name)
+            if not option.fatal:
+                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)
@@ -81,7 +86,10 @@
             # 3. op value == parser value == env value or not env value
 
             op_value = getattr(options, opt_name(option))
-            parser_value = parser.get(section.name, option.name)
+            try:
+                parser_value = parser.get(section.name, option.name)
+            except (NoSectionError, NoOptionError):
+                parser_value = None
             env_value = os.environ.get("CONFIGGLUE_{0}".format(
                 long_name(option).upper()))
 
@@ -111,5 +119,5 @@
     parser, opts, args = schemaconfigglue(scp, op=op)
     is_valid, reasons = scp.is_valid(report=True)
     if not is_valid:
-        parser.error(reasons[0])
+        parser.error('\n'.join(reasons))
     return SchemaGlue(scp, parser, opts, args)

=== modified file 'configglue/pyschema/parser.py'
--- configglue/pyschema/parser.py	2011-06-19 16:38:52 +0000
+++ configglue/pyschema/parser.py	2011-06-22 23:11:36 +0000
@@ -89,13 +89,19 @@
             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)
+                if len(unmatched_sections) > 0:
+                    error_msg = "Sections in configuration are missing from schema: %s"
+                    error_value = ', '.join(unmatched_sections)
+                    errors.append(error_msg % error_value)
+                unmatched_sections = list(schema_sections - parsed_sections)
+                if len(unmatched_sections) > 0:
+                    error_msg = "Sections in schema are missing from configuration: %s"
+                    error_value = ', '.join(unmatched_sections)
+                    errors.append(error_msg % error_value)
             valid &= sections_match
 
-            for name in parsed_sections:
+            for name in list(parsed_sections.union(schema_sections)):
                 if name not in skip_sections:
                     if not self.schema.has_section(name):
                         # this should have been reported before
@@ -103,7 +109,10 @@
                         continue
 
                     section = self.schema.section(name)
-                    parsed_options = set(self.options(name))
+                    try:
+                        parsed_options = set(self.options(name))
+                    except:
+                        parsed_options = set([])
                     schema_options = set(section.options())
 
                     fatal_options = set(opt.name for opt in schema_options
@@ -139,9 +148,8 @@
             self.parse_all()
 
         except Exception, e:
-            if valid:
-                errors.append(e)
-                valid = False
+            errors.append(str(e))
+            valid = False
 
         if report:
             return valid, errors
@@ -382,17 +390,18 @@
         """Return the location (file) where the option was last defined."""
         return self._location.get(option)
 
+    def _extract_interpolation_keys(self, item):
+        if isinstance(item, (list, tuple)):
+            keys = map(self._extract_interpolation_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
+
     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:
@@ -401,7 +410,7 @@
         except:
             value = rawval
 
-        keys = extract_keys(value)
+        keys = self._extract_interpolation_keys(value)
         return rawval, keys
 
     def _interpolate_value(self, section, option):
@@ -439,42 +448,33 @@
     def interpolate_environment(self, rawval, raw=False):
         if raw:
             return rawval
+
         # interpolate environment variables
         pattern = re.sub(r'\${([A-Z_]+)}', r'%(\1)s', rawval)
         pattern = re.sub(r'\$([A-Z_]+)', r'%(\1)s', pattern)
+
+        keys = self._extract_interpolation_keys(pattern)
+        if not keys:
+            # interpolation keys are not valid
+            return rawval
+
         interpolated = pattern % os.environ
         return interpolated
 
     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__':
+        if 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:
-            return value
+            return value
+
+        # any other section
+        opt = self.schema.section(section).option(option)
+        if not opt.fatal:
+            value = opt.default
+            return value
+
+        # no default value found, raise an error
+        raise NoOptionError(option, section)
 
     def get(self, section, option, raw=False, vars=None, parse=True):
         """Return the parsed value of an option.
@@ -507,27 +507,21 @@
             # 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)
 
         # interpolate environment variables
         if isinstance(value, basestring):
             try:
                 value = self.interpolate_environment(value, raw=raw)
+                if parse:
+                    value = self.parse(section, option, value)
             except KeyError:
                 # interpolation failed, fallback to default value
                 value = self._get_default(section, option)
 
-        if parse:
-            value = self.parse(section, option, value)
         return value
 
     def _get_option(self, section, option):
@@ -545,6 +539,10 @@
         # cast value to a string because SafeConfigParser only allows
         # strings to be set
         str_value = option_obj.to_string(value)
+        if not self.has_section(section):
+            # Don't call .add_section here because 2.6 complains
+            # about sections called '__main__'
+            self._sections[section] = {}
         super(SchemaConfigParser, self).set(section, option, str_value)
         filename = self.locate(option)
         self._dirty[filename][section][option] = str_value

=== modified file 'configglue/pyschema/schema.py'
--- configglue/pyschema/schema.py	2011-06-19 16:46:01 +0000
+++ configglue/pyschema/schema.py	2011-06-22 23:11:36 +0000
@@ -15,6 +15,10 @@
 #
 ###############################################################################
 
+from ConfigParser import (
+    NoSectionError,
+    NoOptionError,
+)
 from copy import deepcopy
 from inspect import getmembers
 from warnings import warn
@@ -175,7 +179,8 @@
     def section(self, name):
         """Return a Section by name"""
         section = self._sections.get(name)
-        assert section is not None, "Invalid Section name '%s'" % name
+        if section is None:
+            raise NoSectionError(name)
         return section
 
     def sections(self):
@@ -242,7 +247,8 @@
     def option(self, name):
         """Return a Option by name"""
         opt = getattr(self, name, None)
-        assert opt is not None, "Invalid Option name '%s'" % name
+        if opt is None:
+            raise NoOptionError(name, self.name)
         return opt
 
     def options(self):
@@ -478,10 +484,12 @@
         return result
 
     def to_string(self, value):
+        if value is None and self.null:
+            return 'None'
         return value
 
     def validate(self, value):
-        return isinstance(value, basestring)
+        return (self.null and value is None) or isinstance(value, basestring)
 
 
 class TupleOption(Option):

=== modified file 'configglue/tests/pyschema/test_parser.py'
--- configglue/tests/pyschema/test_parser.py	2011-06-19 17:38:47 +0000
+++ configglue/tests/pyschema/test_parser.py	2011-06-22 23:11:36 +0000
@@ -705,14 +705,12 @@
         self.assertEqual(default, expected)
 
     def test_get_default_no_option(self):
-        expected = None
-        default = self.parser._get_default('__main__', 'bar')
-        self.assertEqual(default, expected)
+        self.assertRaises(NoOptionError, self.parser._get_default,
+            '__main__', 'bar')
 
     def test_get_default_no_section(self):
-        expected = None
-        default = self.parser._get_default('foo', 'bar')
-        self.assertEqual(default, expected)
+        self.assertRaises(NoSectionError, self.parser._get_default,
+            'foo', 'bar')
 
     def test_multi_file_dict_config(self):
         """Test parsing a dict option spanning multiple files."""
@@ -1023,8 +1021,8 @@
 
         valid, errors = self.parser.is_valid(report=True)
         self.assertFalse(valid)
-        self.assertEqual(errors,
-            [u'Sections in configuration do not match schema: bar'])
+        self.assertEqual(errors[0],
+            u'Sections in configuration are missing from schema: bar')
 
     def test_different_sections(self):
         config = StringIO("[__main__]\nfoo=1\n[bar]\nbaz=2")

=== modified file 'configglue/tests/pyschema/test_schema.py'
--- configglue/tests/pyschema/test_schema.py	2011-06-19 16:46:01 +0000
+++ configglue/tests/pyschema/test_schema.py	2011-06-22 23:11:36 +0000
@@ -17,6 +17,7 @@
 ###############################################################################
 
 import unittest
+from ConfigParser import NoOptionError
 from StringIO import StringIO
 
 from configglue.pyschema.parser import (
@@ -351,6 +352,24 @@
         """Test OptionString validate a non-string value."""
         self.assertEqual(self.opt.validate(0), False)
 
+    def test_validate_null_string(self):
+        """Test OptionString validate a null string."""
+        opt = StringOption(null=True)
+        self.assertEqual(opt.validate(None), True)
+
+    def test_to_string_for_null_string(self):
+        """Test OptionString to_string for a null string."""
+        opt = StringOption(null=True)
+        self.assertEqual(opt.to_string(None), 'None')
+        self.assertEqual(self.opt.to_string(''), '')
+        self.assertEqual(self.opt.to_string('foo'), 'foo')
+
+    def test_to_string_for_non_null_string(self):
+        """Test OptionString to_string for non-null string."""
+        self.assertEqual(self.opt.to_string(None), None)
+        self.assertEqual(self.opt.to_string(''), '')
+        self.assertEqual(self.opt.to_string('foo'), 'foo')
+
 
 class TestStringConfigOption(TestStringOption):
     cls = StringConfigOption
@@ -962,7 +981,7 @@
         section.foo = IntOption()
 
         self.assertEqual(section.option('foo'), section.foo)
-        self.assertRaises(AssertionError, section.option, 'bar')
+        self.assertRaises(NoOptionError, section.option, 'bar')
 
     def test_options(self):
         """Test Section options method."""

=== modified file 'configglue/tests/pyschema/test_schemaconfig.py'
--- configglue/tests/pyschema/test_schemaconfig.py	2011-06-19 17:38:47 +0000
+++ configglue/tests/pyschema/test_schemaconfig.py	2011-06-22 23:11:36 +0000
@@ -263,6 +263,28 @@
         output = stdout.read()
         self.assertTrue(output.startswith('Usage:'))
 
+    def test_help_with_fatal(self):
+        """Test schemaconfigglue with --help and an undefined fatal option."""
+        class MySchema(Schema):
+            foo = IntOption(fatal=True)
+
+        self.parser = SchemaConfigParser(MySchema())
+
+        # 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:'))
+
     def test_parser_set_with_encoding(self):
         """Test schemaconfigglue override an option with a non-ascii value."""
         class MySchema(Schema):


Follow ups