diff --git a/IPython/config/configurable.py b/IPython/config/configurable.py index 0413c8a..60f79e9 100644 --- a/IPython/config/configurable.py +++ b/IPython/config/configurable.py @@ -28,7 +28,7 @@ Authors: import datetime from copy import deepcopy -from loader import Config +from .loader import Config, LazyConfigValue from IPython.utils.traitlets import HasTraits, Instance from IPython.utils.text import indent, wrap_paragraphs @@ -137,7 +137,7 @@ class Configurable(HasTraits): 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""" @@ -149,6 +149,11 @@ class Configurable(HasTraits): my_config = self._find_my_config(cfg) for name, config_value in my_config.iteritems(): if name in traits: + if isinstance(config_value, LazyConfigValue): + # ConfigValue is a wrapper for using append / update on containers + # without having to copy the + initial = getattr(self, name) + config_value = config_value.get_value(initial) # 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. diff --git a/IPython/config/loader.py b/IPython/config/loader.py index 919531a..2076929 100644 --- a/IPython/config/loader.py +++ b/IPython/config/loader.py @@ -25,6 +25,7 @@ Authors import __builtin__ as builtin_mod import argparse +import copy import os import re import sys @@ -32,6 +33,7 @@ import sys from IPython.utils.path import filefind, get_ipython_dir from IPython.utils import py3compat, warn from IPython.utils.encoding import DEFAULT_ENCODING +from IPython.utils.traitlets import HasTraits, List, Any, TraitError #----------------------------------------------------------------------------- # Exceptions @@ -74,6 +76,83 @@ class ArgumentParser(argparse.ArgumentParser): # Config class for holding config information #----------------------------------------------------------------------------- +class LazyConfigValue(HasTraits): + """Proxy object for exposing methods on configurable containers + + Exposes: + + - append, extend, insert on lists + - update on dicts + - update, add on sets + """ + + _value = None + + # list methods + _extend = List() + + def append(self, obj): + self._extend.append(obj) + + def extend(self, other): + self._extend.extend(other) + + _inserts = List() + def insert(self, index, other): + if not isinstance(index, int): + raise TypeError("An integer is required") + self._inserts.append((index, other)) + + # dict methods + # update is used for both dict and set + _update = Any() + def update(self, other): + if self._update is None: + if isinstance(other, dict): + self._update = {} + else: + self._update = set() + self._update.update(other) + + # set methods + def add(self, obj): + self.update({obj}) + + def get_value(self, initial): + """construct the value from the initial one + + after applying any insert / extend / update changes + """ + if self._value is not None: + return self._value + value = copy.deepcopy(initial) + if isinstance(value, list): + for idx, obj in self._inserts: + value.insert(idx, obj) + value.extend(self._extend) + elif isinstance(value, dict): + if self._update: + value.update(self._update) + elif isinstance(value, set): + if self._update: + value.update(self._update) + self._value = value + return value + + def to_dict(self): + """return JSONable dict form of my data + + Currently update as dict or set, extend as list, and inserts as list of tuples. + """ + d = {} + if self._update: + d['update'] = self._update + if self._extend: + d['extend'] = self._extend + elif self._inserts: + d['inserts'] = self._inserts + return d + class Config(dict): """An attribute based dict that can do smart merges.""" @@ -125,6 +204,14 @@ class Config(dict): return False def __contains__(self, key): + # allow nested contains of the form `Section.key in config` + if '.' in key: + first, remainder = key.split('.', 1) + if first not in self: + return False + return remainder in self[first] + + # we always have Sections if self._is_section_key(key): return True else: @@ -147,7 +234,7 @@ class Config(dict): def __deepcopy__(self, memo): import copy return type(self)(copy.deepcopy(self.items())) - + def __getitem__(self, key): # We cannot use directly self._is_section_key, because it triggers # infinite recursion on top of PyPy. Instead, we manually fish the @@ -170,7 +257,14 @@ class Config(dict): dict.__setitem__(self, key, c) return c else: - return dict.__getitem__(self, key) + try: + return dict.__getitem__(self, key) + except KeyError: + # undefined + v = LazyConfigValue() + dict.__setitem__(self, key, v) + return v + def __setitem__(self, key, value): if self._is_section_key(key):