diff --git a/IPython/config/configurable.py b/IPython/config/configurable.py index f5b7283..84b2a64 100644 --- a/IPython/config/configurable.py +++ b/IPython/config/configurable.py @@ -64,8 +64,8 @@ class Configurable(HasTraits): If this is empty, default values are used. If config is a :class:`Config` instance, it will be used to configure the instance. - parent : Configurable instance - The parent + parent : Configurable instance, optional + The parent Configurable instance of this object. Notes ----- @@ -112,6 +112,31 @@ class Configurable(HasTraits): return [c.__name__ for c in reversed(cls.__mro__) if issubclass(c, Configurable) and issubclass(cls, c) ] + + def _find_my_config(self, cfg): + """extract my config from a global Config object + + will construct a Config object of only the config values that apply to me + based on my mro(), as well as those of my parent(s) if they exist. + + If I am Bar and my parent is Foo, and their parent is Tim, + this will return merge following config sections, in this order:: + + [Bar, Foo.bar, Tim.Foo.Bar] + + With the last item being the highest priority. + """ + cfgs = [cfg] + if self.parent: + cfgs.append(self.parent._find_my_config(cfg)) + my_config = Config() + for c in cfgs: + for sname in self.section_names(): + # Don't do a blind getattr as that would cause the config to + # dynamically create the section with name Class.__name__. + if c._has_section(sname): + my_config.merge(c[sname]) + return my_config def _load_config(self, cfg, section_names=None, traits=None): """load traits from a Config object""" @@ -121,24 +146,13 @@ class Configurable(HasTraits): if section_names is None: section_names = self.section_names() - for sname in section_names: - # Don't do a blind getattr as that would cause the config to - # dynamically create the section with name self.__class__.__name__. - if cfg._has_section(sname): - my_config = cfg[sname] - for k, v in traits.iteritems(): - try: - # Here we grab the value from the config - # If k has the naming convention of a config - # section, it will be auto created. - config_value = my_config[k] - except KeyError: - pass - else: - # We have to do a deepcopy here if we don't deepcopy the entire - # config object. If we don't, a mutable config_value will be - # shared by all instances, effectively making it a class attribute. - setattr(self, k, deepcopy(config_value)) + my_config = self._find_my_config(cfg) + for name, config_value in my_config.iteritems(): + if name in traits: + # We have to do a deepcopy here if we don't deepcopy the entire + # config object. If we don't, a mutable config_value will be + # shared by all instances, effectively making it a class attribute. + setattr(self, name, deepcopy(config_value)) def _config_changed(self, name, old, new): """Update all the class traits having ``config=True`` as metadata. @@ -155,13 +169,6 @@ class Configurable(HasTraits): # and works down the mro loading the config for each section. section_names = self.section_names() self._load_config(new, traits=traits, section_names=section_names) - - # load parent config as well, if we have one - parent_section_names = [] if self.parent is None else self.parent.section_names() - for parent in parent_section_names: - if not new._has_section(parent): - continue - self._load_config(new[parent], traits=traits, section_names=section_names) def update_config(self, config): """Fire the traits events when the config is updated.""" diff --git a/IPython/config/tests/test_configurable.py b/IPython/config/tests/test_configurable.py index e52fdaa..88b087d 100644 --- a/IPython/config/tests/test_configurable.py +++ b/IPython/config/tests/test_configurable.py @@ -192,7 +192,6 @@ class MyParent2(MyParent): class TestParentConfigurable(TestCase): def test_parent_config(self): - cfg = Config({ 'MyParent' : { 'MyConfigurable' : { @@ -205,7 +204,6 @@ class TestParentConfigurable(TestCase): self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b) def test_parent_inheritance(self): - cfg = Config({ 'MyParent' : { 'MyConfigurable' : { @@ -217,8 +215,26 @@ class TestParentConfigurable(TestCase): myc = MyConfigurable(parent=parent) self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b) + def test_multi_parent(self): + cfg = Config({ + 'MyParent2' : { + 'MyParent' : { + 'MyConfigurable' : { + 'b' : 2.0, + } + }, + # this one shouldn't count + 'MyConfigurable' : { + 'b' : 3.0, + }, + } + }) + parent2 = MyParent2(config=cfg) + parent = MyParent(parent=parent2) + myc = MyConfigurable(parent=parent) + self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b) + def test_parent_priority(self): - cfg = Config({ 'MyConfigurable' : { 'b' : 2.0, @@ -237,3 +253,32 @@ class TestParentConfigurable(TestCase): parent = MyParent2(config=cfg) myc = MyConfigurable(parent=parent) self.assertEqual(myc.b, parent.config.MyParent2.MyConfigurable.b) + + def test_multi_parent_priority(self): + cfg = Config({ + 'MyConfigurable' : { + 'b' : 2.0, + }, + 'MyParent' : { + 'MyConfigurable' : { + 'b' : 3.0, + } + }, + 'MyParent2' : { + 'MyConfigurable' : { + 'b' : 4.0, + } + }, + 'MyParent2' : { + 'MyParent' : { + 'MyConfigurable' : { + 'b' : 5.0, + } + } + } + }) + parent2 = MyParent2(config=cfg) + parent = MyParent2(parent=parent2) + myc = MyConfigurable(parent=parent) + self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b) +