← Back to team overview

configglue team mailing list archive

[Merge] lp:~ricardokirkner/configglue/declarative-syntax into lp:configglue

 

Ricardo Kirkner has proposed merging lp:~ricardokirkner/configglue/declarative-syntax into lp:configglue.

Requested reviews:
  Configglue developers (configglue)

For more details, see:
https://code.launchpad.net/~ricardokirkner/configglue/declarative-syntax/+merge/55003

This branch fixes proper schema inheritance using declarative syntax.

For example:

class SchemaA(Schema):
    foo = ConfigSection()
    foo.bar = IntConfigOption()

class SchemaB(SchemaA):
    foo = ConfigSection()
    foo.baz = BoolConfigOption()

In this code, the SchemaB class would have a section called 'foo' with *two* options, 'bar' and 'baz'. When initializing the class instance, all attributes get merged so that they get correctly overridden by the subclass' attributes.

It's necessary to have the section duplicate, as not having it results in a syntax error. By merging sections together at initialization, the end effect is the desired one, however.
-- 
https://code.launchpad.net/~ricardokirkner/configglue/declarative-syntax/+merge/55003
Your team Configglue developers is requested to review the proposed merge of lp:~ricardokirkner/configglue/declarative-syntax into lp:configglue.
=== modified file 'configglue/pyschema/schema.py'
--- configglue/pyschema/schema.py	2011-03-19 16:31:33 +0000
+++ configglue/pyschema/schema.py	2011-03-27 14:47:23 +0000
@@ -16,10 +16,10 @@
 ###############################################################################
 
 from copy import deepcopy
+from inspect import getmembers
 
 
 __all__ = [
-    'super_vars',
     'BoolConfigOption',
     'ConfigOption',
     'ConfigSection',
@@ -36,15 +36,39 @@
 _internal = object.__dict__.keys() + ['__module__']
 
 
-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
+def get_config_objects(obj):
+    objects = ((n, o) for (n, o) in getmembers(obj)
+        if isinstance(o, (ConfigSection, ConfigOption)))
+    return objects
+
+
+def merge(*schemas):
+    # define result schema
+    class MergedSchema(Schema):
+        pass
+
+    def add_to_object(obj, name, value):
+        if isinstance(value, ConfigSection):
+            if not hasattr(obj, name):
+                # name not found in section, just add it
+                setattr(obj, name, value)
+            else:
+                # name found, let's merge it's children
+                parent = getattr(obj, name)
+                for child_name, child_value in get_config_objects(value):
+                    add_to_object(parent, child_name, child_value)
+        else:
+            # don't override ConfigOption objects if they already exist
+            if not hasattr(obj, name):
+                setattr(obj, name, value)
+
+    # for each schema
+    for schema in schemas:
+        # process top-level objects
+        for name, obj in get_config_objects(schema):
+            add_to_object(MergedSchema, name, obj)
+
+    return MergedSchema
 
 
 class Schema(object):
@@ -65,31 +89,26 @@
     configuration files.
     """
 
-    def __new__(cls):
-        instance = super(Schema, cls).__new__(cls)
+    def __init__(self):
+        bases = (c for c in self.__class__.__mro__ if issubclass(c, Schema))
+        # get merged schema so that all inherited attributes are
+        # included
+        merged = merge(*bases)
 
         # override class attributes with instance attributes to correctly
         # handle schema inheritance
-        schema_attributes = filter(
-            lambda x: x not in _internal and
-                isinstance(getattr(cls, x), (ConfigSection, ConfigOption)),
-            super_vars(cls))
-        for attr in schema_attributes:
-            setattr(instance, attr, deepcopy(getattr(cls, attr)))
-
-        return instance
-
-    def __init__(self):
+        for name, value in get_config_objects(merged):
+            setattr(self, name, deepcopy(value))
+
         self.includes = LinesConfigOption(item=StringConfigOption())
         self._sections = {}
         defaultSection = None
-        for attname in super_vars(self.__class__):
-            att = getattr(self, attname)
+        for name, value in get_config_objects(self.__class__):
+            att = getattr(self, name)
             if isinstance(att, ConfigSection):
-                att.name = attname
-                self._sections[attname] = att
-                for optname in super_vars(att):
-                    opt = getattr(att, optname)
+                att.name = name
+                self._sections[name] = att
+                for optname, opt in get_config_objects(att):
                     if isinstance(opt, ConfigOption):
                         opt.name = optname
                         opt.section = att
@@ -98,9 +117,9 @@
                     defaultSection = ConfigSection()
                     defaultSection.name = '__main__'
                     self._sections['__main__'] = defaultSection
-                att.name = attname
+                att.name = name
                 att.section = defaultSection
-                setattr(defaultSection, attname, att)
+                setattr(defaultSection, name, att)
 
     def __eq__(self, other):
         return (self._sections == other._sections and
@@ -140,7 +159,8 @@
             for s in self.sections():
                 options += self.options(s)
         elif section.name == '__main__':
-            options = [getattr(self, att) for att in super_vars(self.__class__) 
+            class_config_objects = get_config_objects(self.__class__)
+            options = [getattr(self, att) for att, _ in class_config_objects
                            if isinstance(getattr(self, att), ConfigOption)]
         else:
             options = section.options()

=== modified file 'tests/pyschema/test_schema.py'
--- tests/pyschema/test_schema.py	2011-03-19 16:31:33 +0000
+++ tests/pyschema/test_schema.py	2011-03-27 14:47:23 +0000
@@ -29,6 +29,7 @@
     Schema,
     StringConfigOption,
     TupleConfigOption,
+    merge,
 )
 
 
@@ -154,6 +155,57 @@
         # test on the other schema
         self.assertFalse(hasattr(self.other.foo, 'baz'))
 
+    def test_merge(self):
+        class SchemaA(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+            bar = IntConfigOption()
+
+        class SchemaB(Schema):
+            foo = ConfigSection()
+            foo.baz = IntConfigOption()
+
+        class SchemaC(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+            foo.baz = IntConfigOption()
+            bar = IntConfigOption()
+
+        expected = SchemaC()
+        merged = merge(SchemaA, SchemaB)()
+
+        self.assertEqual(merged.sections(), expected.sections())
+        self.assertEqual(merged.options(), expected.options())
+
+    def test_merge_inherited(self):
+        class SchemaA(Schema):
+            foo = ConfigSection()
+            foo.bar = IntConfigOption()
+            bar = IntConfigOption()
+
+        class SchemaB(SchemaA):
+            foo = ConfigSection()
+            foo.baz = IntConfigOption()
+
+        # SchemaB inherits attributes from SchemaA and merges its own
+        # attributes into
+        schema = SchemaB()
+        section_names = set(s.name for s in schema.sections())
+        option_names = set(o.name for o in schema.options('__main__'))
+        foo_option_names = set(o.name for o in schema.options('foo'))
+        self.assertEqual(section_names, set(['__main__', 'foo']))
+        self.assertEqual(option_names, set(['bar']))
+        self.assertEqual(foo_option_names, set(['bar', 'baz']))
+
+        # SchemaB inheritance does not affect SchemaA
+        schema = SchemaA()
+        section_names = set(s.name for s in schema.sections())
+        option_names = set(o.name for o in schema.options('__main__'))
+        foo_option_names = set(o.name for o in schema.options('foo'))
+        self.assertEqual(section_names, set(['__main__', 'foo']))
+        self.assertEqual(option_names, set(['bar']))
+        self.assertEqual(foo_option_names, set(['bar']))
+
 
 class TestStringConfigOption(unittest.TestCase):
     def setUp(self):


Follow ups