Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add parent to Configurable #3430

Merged
merged 6 commits into from
Jul 1, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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