Skip to content

Commit

Permalink
Merge pull request ipython#3430 from minrk/configinstance
Browse files Browse the repository at this point in the history
add parent to Configurable
  • Loading branch information
ellisonbg committed Jul 1, 2013
2 parents 81f09c1 + 30d584c commit 096207a
Show file tree
Hide file tree
Showing 33 changed files with 243 additions and 116 deletions.
94 changes: 60 additions & 34 deletions IPython/config/configurable.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class MultipleInstanceError(ConfigurableError):
class Configurable(HasTraits):

config = Instance(Config, (), {})
parent = Instance('IPython.config.configurable.Configurable')
created = None

def __init__(self, **kwargs):
Expand All @@ -63,6 +64,8 @@ def __init__(self, **kwargs):
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, optional
The parent Configurable instance of this object.
Notes
-----
Expand All @@ -77,6 +80,13 @@ def __init__(self, config=None):
This ensures that instances will be configured properly.
"""
parent = kwargs.pop('parent', None)
if parent is not None:
# config is implied from parent
if kwargs.get('config', None) is None:
kwargs['config'] = parent.config
self.parent = parent

config = kwargs.pop('config', None)
if config is not None:
# We used to deepcopy, but for now we are trying to just save
Expand All @@ -95,6 +105,54 @@ def __init__(self, config=None):
#-------------------------------------------------------------------------
# Static trait notifiations
#-------------------------------------------------------------------------

@classmethod
def section_names(cls):
"""return section names as a list"""
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"""

if traits is None:
traits = self.traits(config=True)
if section_names is None:
section_names = self.section_names()

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.
Expand All @@ -109,39 +167,8 @@ def _config_changed(self, name, old, new):
# We auto-load config section for this class as well as any parent
# classes that are Configurable subclasses. This starts with Configurable
# and works down the mro loading the config for each section.
section_names = [cls.__name__ for cls in \
reversed(self.__class__.__mro__) if
issubclass(cls, Configurable) and issubclass(self.__class__, cls)]

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 new._has_section(sname):
my_config = new[sname]
for k, v in traits.iteritems():
# Don't allow traitlets with config=True to start with
# uppercase. Otherwise, they are confused with Config
# subsections. But, developers shouldn't have uppercase
# attributes anyways! (PEP 6)
if k[0].upper()==k[0] and not k.startswith('_'):
raise ConfigurableError('Configurable traitlets with '
'config=True must start with a lowercase so they are '
'not confused with Config subsections: %s.%s' % \
(self.__class__.__name__, k))
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:
# print "Setting %s.%s from %s.%s=%r" % \
# (self.__class__.__name__,k,sname,k,config_value)
# 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))
section_names = self.section_names()
self._load_config(new, traits=traits, section_names=section_names)

def update_config(self, config):
"""Fire the traits events when the config is updated."""
Expand All @@ -161,7 +188,6 @@ def class_get_help(cls, inst=None):
class defaults.
"""
assert inst is None or isinstance(inst, cls)
cls_traits = cls.class_traits(config=True)
final_help = []
final_help.append(u'%s options' % cls.__name__)
final_help.append(len(final_help[0])*u'-')
Expand Down
4 changes: 2 additions & 2 deletions IPython/config/tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ class MyApp(Application):
))

def init_foo(self):
self.foo = Foo(config=self.config)
self.foo = Foo(parent=self)

def init_bar(self):
self.bar = Bar(config=self.config)
self.bar = Bar(parent=self)


class TestApplication(TestCase):
Expand Down
101 changes: 101 additions & 0 deletions IPython/config/tests/test_configurable.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,104 @@ class Bam(Bar): pass
self.assertEqual(bam, Bam._instance)
self.assertEqual(bam, Bar._instance)
self.assertEqual(SingletonConfigurable._instance, None)


class MyParent(Configurable):
pass

class MyParent2(MyParent):
pass

class TestParentConfigurable(TestCase):

def test_parent_config(self):
cfg = Config({
'MyParent' : {
'MyConfigurable' : {
'b' : 2.0,
}
}
})
parent = MyParent(config=cfg)
myc = MyConfigurable(parent=parent)
self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)

def test_parent_inheritance(self):
cfg = Config({
'MyParent' : {
'MyConfigurable' : {
'b' : 2.0,
}
}
})
parent = MyParent2(config=cfg)
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,
},
'MyParent' : {
'MyConfigurable' : {
'b' : 3.0,
}
},
'MyParent2' : {
'MyConfigurable' : {
'b' : 4.0,
}
}
})
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)

4 changes: 2 additions & 2 deletions IPython/consoleapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def init_kernel_manager(self):
stdin_port=self.stdin_port,
hb_port=self.hb_port,
connection_file=self.connection_file,
config=self.config,
parent=self,
)
self.kernel_manager.client_factory = self.kernel_client_class
self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
Expand Down Expand Up @@ -371,7 +371,7 @@ def init_kernel_client(self):
stdin_port=self.stdin_port,
hb_port=self.hb_port,
connection_file=self.connection_file,
config=self.config,
parent=self,
)

self.kernel_client.start_channels()
Expand Down
4 changes: 2 additions & 2 deletions IPython/core/alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ class AliasManager(Configurable):
user_aliases = List(default_value=[], config=True)
shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')

def __init__(self, shell=None, config=None):
super(AliasManager, self).__init__(shell=shell, config=config)
def __init__(self, shell=None, **kwargs):
super(AliasManager, self).__init__(shell=shell, **kwargs)
self.alias_table = {}
self.exclude_aliases()
self.init_aliases()
Expand Down
4 changes: 2 additions & 2 deletions IPython/core/completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class Completer(Configurable):
)


def __init__(self, namespace=None, global_namespace=None, config=None, **kwargs):
def __init__(self, namespace=None, global_namespace=None, **kwargs):
"""Create a new completer for the command line.
Completer(namespace=ns,global_namespace=ns2) -> completer instance.
Expand Down Expand Up @@ -280,7 +280,7 @@ def __init__(self, namespace=None, global_namespace=None, config=None, **kwargs)
else:
self.global_namespace = global_namespace

super(Completer, self).__init__(config=config, **kwargs)
super(Completer, self).__init__(**kwargs)

def complete(self, text, state):
"""Return the next possible completion for 'text'.
Expand Down
4 changes: 2 additions & 2 deletions IPython/core/displayhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class DisplayHook(Configurable):

shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')

def __init__(self, shell=None, cache_size=1000, config=None):
super(DisplayHook, self).__init__(shell=shell, config=config)
def __init__(self, shell=None, cache_size=1000, **kwargs):
super(DisplayHook, self).__init__(shell=shell, **kwargs)

cache_size_min = 3
if cache_size <= 0:
Expand Down
6 changes: 3 additions & 3 deletions IPython/core/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def load_ipython_extension(ipython):
that point, including defining new magic and aliases, adding new
components, etc.
You can also optionaly define an :func:`unload_ipython_extension(ipython)`
You can also optionally define an :func:`unload_ipython_extension(ipython)`
function, which will be called if the user unloads or reloads the extension.
The extension manager will only call :func:`load_ipython_extension` again
if the extension is reloaded.
Expand All @@ -61,8 +61,8 @@ def load_ipython_extension(ipython):

shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')

def __init__(self, shell=None, config=None):
super(ExtensionManager, self).__init__(shell=shell, config=config)
def __init__(self, shell=None, **kwargs):
super(ExtensionManager, self).__init__(shell=shell, **kwargs)
self.shell.on_trait_change(
self._on_ipython_dir_changed, 'ipython_dir'
)
Expand Down
2 changes: 1 addition & 1 deletion IPython/core/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def _formatters_default(self):
]
d = {}
for cls in formatter_classes:
f = cls(config=self.config)
f = cls(parent=self)
d[f.format_type] = f
return d

Expand Down
4 changes: 2 additions & 2 deletions IPython/core/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def _db_changed(self, name, old, new):
(self.__class__.__name__, new)
raise TraitError(msg)

def __init__(self, profile='default', hist_file=u'', config=None, **traits):
def __init__(self, profile='default', hist_file=u'', **traits):
"""Create a new history accessor.
Parameters
Expand All @@ -162,7 +162,7 @@ def __init__(self, profile='default', hist_file=u'', config=None, **traits):
Config object. hist_file can also be set through this.
"""
# We need a pointer back to the shell for various tasks.
super(HistoryAccessor, self).__init__(config=config, **traits)
super(HistoryAccessor, self).__init__(**traits)
# defer setting hist_file from kwarg until after init,
# otherwise the default kwarg value would clobber any value
# set by config
Expand Down

0 comments on commit 096207a

Please sign in to comment.