From 9ce3a358ab9638adf1cdab30aff01436c6fb64aa 2008-06-10 01:18:40
From: Stefan van der Walt <bzr@mentat.za.net>
Date: 2008-06-10 01:18:40
Subject: [PATCH] Merge sconfig branch.

---

diff --git a/IPython/config/api.py b/IPython/config/api.py
index 1fca60e..a24a42c 100644
--- a/IPython/config/api.py
+++ b/IPython/config/api.py
@@ -19,6 +19,11 @@ import os
 from IPython.config.cutils import get_home_dir, get_ipython_dir
 from IPython.external.configobj import ConfigObj
 
+# Traitlets config imports
+from IPython.config import traitlets
+from IPython.config.config import *
+from traitlets import *
+
 class ConfigObjManager(object):
     
     def __init__(self, configObj, filename):
diff --git a/IPython/config/config.py b/IPython/config/config.py
new file mode 100644
index 0000000..e88fb8a
--- /dev/null
+++ b/IPython/config/config.py
@@ -0,0 +1,179 @@
+"""Load and store configuration objects.
+
+Test this module using
+
+nosetests -v --with-doctest --doctest-tests IPython.config
+
+"""
+
+from __future__ import with_statement
+from contextlib import contextmanager
+
+import inspect
+import types
+from IPython.config import traitlets
+from traitlets import Traitlet
+from IPython.external.configobj import ConfigObj
+
+def debug(s):
+    import sys
+    sys.stderr.write(str(s) + '\n')
+
+@contextmanager
+def raw(config):
+    """Context manager for accessing traitlets directly.
+
+    """
+    config.__getattribute__('',raw_access=True)
+    yield config
+    config.__getattribute__('',raw_access=False)
+
+class Config(object):
+    """
+    Implementation Notes
+    ====================
+    All instances of the same Config class share properties.  Therefore,
+
+    >>> class Sample(Config):
+    ...     my_float = traitlets.Float(3)
+
+    >>> s0 = Sample()
+    >>> s1 = Sample()
+    >>> s0.my_float = 5
+    >>> s0.my_float == s1.my_float
+    True
+
+    """
+    def __init__(self):
+        # Instantiate subconfigs
+        with raw(self):
+            subconfigs = [(n,v) for n,v in
+                          inspect.getmembers(self, inspect.isclass)
+                          if not n.startswith('__')]
+
+        for n,v in subconfigs:
+            setattr(self, n, v())
+
+    def __getattribute__(self,attr,raw_access=None,
+                         _ns={'raw_access':False}):
+        if raw_access is not None:
+            _ns['raw_access'] = raw_access
+            return
+
+        obj = object.__getattribute__(self,attr)
+        if isinstance(obj,Traitlet) and not _ns['raw_access']:
+            return obj.__call__()
+        else:
+            return obj
+
+    def __setattr__(self,attr,value):
+        obj = object.__getattribute__(self,attr)
+        if isinstance(obj,Traitlet):
+            obj(value)
+        else:
+            self.__dict__[attr] = value
+
+    def __str__(self,level=1,only_modified=True):
+        ci = ConfigInspector(self)
+        out = ''
+        spacer = '  '*(level-1)
+
+        # Add traitlet representations
+        for p,v in ci.properties:
+            if (v.modified and only_modified) or not only_modified:
+                out += spacer + '%s = %s\n' % (p,v)
+
+        # Add subconfig representations
+        for (n,v) in ci.subconfigs:
+            sub_str = v.__str__(level=level+1,only_modified=only_modified)
+            if sub_str:
+                out += '\n' + spacer + '[' * level + ('%s' % n) \
+                       + ']'*level + '\n'
+                out += sub_str
+
+        return out
+
+    def __iadd__(self,source):
+        """Load configuration from filename, and update self.
+
+        """
+        if not isinstance(source,dict):
+            source = ConfigObj(source, unrepr=True)
+        update_from_dict(self,source)
+        return self
+
+
+class ConfigInspector(object):
+    """Allow the inspection of Config objects.
+
+    """
+    def __init__(self,config):
+        self._config = config
+
+    @property
+    def properties(self):
+        "Return all traitlet names."
+        with raw(self._config):
+            return inspect.getmembers(self._config,
+                                      lambda obj: isinstance(obj, Traitlet))
+
+    @property
+    def subconfigs(self):
+        "Return all subconfig names and values."
+        with raw(self._config):
+            return [(n,v) for n,v in
+                    inspect.getmembers(self._config,
+                                       lambda obj: isinstance(obj,Config))
+                    if not n.startswith('__')]
+
+    def reset(self):
+        for (p,v) in self.properties:
+            v.reset()
+
+        for (s,v) in self.subconfigs:
+            ConfigInspector(v).reset()
+
+def update_from_dict(config,d):
+    """Propagate the values of the dictionary to the given configuration.
+
+    Useful to load configobj instances.
+
+    """
+    for k,v in d.items():
+        try:
+            prop_or_subconfig = getattr(config, k)
+        except AttributeError:
+            print "Invalid section/property in config file: %s" % k
+        else:
+            if isinstance(v,dict):
+                update_from_dict(prop_or_subconfig,v)
+            else:
+                setattr(config, k, v)
+
+def dict_from_config(config,only_modified=True):
+    """Create a dictionary from a Config object."""
+    ci = ConfigInspector(config)
+    out = {}
+
+    for p,v in ci.properties:
+        if (v.modified and only_modified) or not only_modified:
+            out[p] = v
+
+    for s,v in ci.subconfigs:
+        d = dict_from_config(v,only_modified)
+        if d != {}:
+            out[s] = d
+
+    return out
+
+def write(config, target):
+    """Write a configuration to file.
+
+    """
+    if isinstance(target, str):
+        target = open(target, 'w+')
+    target.flush()
+    target.seek(0)
+
+    confobj = ConfigObj(dict_from_config(config), unrepr=True)
+    confobj.write(target)
diff --git a/IPython/config/sconfig.py b/IPython/config/sconfig.py
deleted file mode 100644
index 886d201..0000000
--- a/IPython/config/sconfig.py
+++ /dev/null
@@ -1,622 +0,0 @@
-# encoding: utf-8
-
-"""Mix of ConfigObj and Struct-like access.
-
-Provides:
-
-- Coupling a Struct object to a ConfigObj one, so that changes to the Traited
-  instance propagate back into the ConfigObj.
-
-- A declarative interface for describing configurations that automatically maps
-  to valid ConfigObj representations.
-
-- From these descriptions, valid .conf files can be auto-generated, with class
-  docstrings and traits information used for initial auto-documentation.
-
-- Hierarchical inclusion of files, so that a base config can be overridden only
-  in specific spots.
-
-
-Notes:
-
-The file creation policy is:
-
-1. Creating a SConfigManager(FooConfig,'missingfile.conf')  will work
-fine, and 'missingfile.conf' will be created empty.
-
-2. Creating SConfigManager(FooConfig,'OKfile.conf') where OKfile.conf has
-
-include = 'missingfile.conf'
-
-conks out with IOError.
-
-My rationale is that creating top-level empty files is a common and
-reasonable need, but that having invalid include statements should
-raise an error right away, so people know immediately that their files
-have gone stale.
-
-
-TODO:
-
-  - Turn the currently interactive tests into proper doc/unit tests.  Complete
-    docstrings. 
-
-  - Write the real ipython1 config system using this.  That one is more
-  complicated than either the MPL one or the fake 'ipythontest' that I wrote
-  here, and it requires solving the issue of declaring references to other
-  objects inside the config files.
-
-  - [Low priority] Write a custom TraitsUI view so that hierarchical
-  configurations provide nicer interactive editing.  The automatic system is
-  remarkably good, but for very complex configurations having a nicely
-  organized view would be nice.
-"""
-
-__docformat__ = "restructuredtext en"
-__license__ = 'BSD'
-
-#-------------------------------------------------------------------------------
-#  Copyright (C) 2008  The IPython Development Team
-#
-#  Distributed under the terms of the BSD License.  The full license is in
-#  the file COPYING, distributed as part of this software.
-#-------------------------------------------------------------------------------
-
-#-------------------------------------------------------------------------------
-# Imports
-#-------------------------------------------------------------------------------
-
-############################################################################
-# Stdlib imports
-############################################################################
-from cStringIO import StringIO
-from inspect import isclass
-
-import os
-import textwrap
-
-############################################################################
-# External imports
-############################################################################
-
-from IPython.external import configobj
-
-############################################################################
-# Utility functions
-############################################################################
-
-def get_split_ind(seq, N):
-    """seq is a list of words.  Return the index into seq such that
-    len(' '.join(seq[:ind])<=N
-    """
-
-    sLen = 0
-    # todo: use Alex's xrange pattern from the cbook for efficiency
-    for (word, ind) in zip(seq, range(len(seq))):
-        sLen += len(word) + 1  # +1 to account for the len(' ')
-        if sLen>=N: return ind
-    return len(seq)
-
-def wrap(prefix, text, cols, max_lines=6):
-    """'wrap text with prefix at length cols"""
-    pad = ' '*len(prefix.expandtabs())
-    available = cols - len(pad)
-
-    seq = text.split(' ')
-    Nseq = len(seq)
-    ind = 0
-    lines = []
-    while ind<Nseq:
-        lastInd = ind
-        ind += get_split_ind(seq[ind:], available)
-        lines.append(seq[lastInd:ind])
-
-    num_lines = len(lines)
-    abbr_end = max_lines // 2
-    abbr_start = max_lines - abbr_end
-    lines_skipped = False
-    for i in range(num_lines):
-        if i == 0:
-            # add the prefix to the first line, pad with spaces otherwise
-            ret = prefix + ' '.join(lines[i]) + '\n'
-        elif i < abbr_start or i > num_lines-abbr_end-1:
-            ret += pad + ' '.join(lines[i]) + '\n'
-        else:
-            if not lines_skipped:
-                lines_skipped = True
-                ret += ' <...snipped %d lines...> \n' % (num_lines-max_lines)
-#    for line in lines[1:]:
-#        ret += pad + ' '.join(line) + '\n'
-    return ret[:-1]
-
-def dedent(txt):
-    """A modified version of textwrap.dedent, specialized for docstrings.
-
-    This version doesn't get confused by the first line of text having
-    inconsistent indentation from the rest, which happens a lot in docstrings.
-
-    :Examples:
-
-        >>> s = '''
-        ... First line.
-        ... More...
-        ... End'''
-
-        >>> print dedent(s)
-        First line.
-        More...
-        End
-
-        >>> s = '''First line
-        ... More...
-        ... End'''
-
-        >>> print dedent(s)
-        First line
-        More...
-        End
-    """
-    out = [textwrap.dedent(t) for t in txt.split('\n',1)
-           if t and not t.isspace()]
-    return '\n'.join(out)
-
-
-def comment(strng,indent=''):
-    """return an input string, commented out"""
-    template = indent + '# %s'
-    lines = [template % s for s in strng.splitlines(True)]
-    return ''.join(lines)
-
-
-def configobj2str(cobj):
-    """Dump a Configobj instance to a string."""
-    outstr = StringIO()
-    cobj.write(outstr)
-    return outstr.getvalue()
-
-def get_config_filename(conf):
-    """Find the filename attribute of a ConfigObj given a sub-section object.
-    """
-    depth = conf.depth
-    for d in range(depth):
-        conf = conf.parent
-    return conf.filename
-
-def sconf2File(sconf,fname,force=False):
-    """Write a SConfig instance to a given filename.
-
-    :Keywords:
-
-      force : bool (False)
-        If true, force writing even if the file exists.
-      """
-    
-    if os.path.isfile(fname) and not force:
-        raise IOError("File %s already exists, use force=True to overwrite" %
-                      fname)
-
-    txt = repr(sconf)
-
-    fobj = open(fname,'w')
-    fobj.write(txt)
-    fobj.close()
-
-def filter_scalars(sc):
-    """ input sc MUST be sorted!!!"""
-    scalars = []
-    maxi = len(sc)-1
-    i = 0
-    while i<len(sc):
-        t = sc[i]
-        if t.startswith('_sconf_'):
-            # Skip altogether private _sconf_ attributes, so we actually issue
-            # a 'continue' call to avoid the append(t) below
-            i += 1
-            continue
-        scalars.append(t)
-        i += 1
-
-    return scalars
-
-
-def get_scalars(obj):
-    """Return scalars for a Sconf class object"""
-
-    skip = set(['trait_added','trait_modified'])
-    sc = [k for k in obj.__dict__ if not k.startswith('_')]
-    sc.sort()
-    return filter_scalars(sc)
-
-
-def get_sections(obj,sectionClass):
-    """Return sections for a Sconf class object"""
-    return [(n,v) for (n,v) in obj.__dict__.iteritems()
-            if isclass(v) and issubclass(v,sectionClass)]
-
-
-def get_instance_sections(inst):
-    """Return sections for a Sconf instance"""
-    sections = [(k,v) for k,v in inst.__dict__.iteritems()
-                if isinstance(v,SConfig) and not k=='_sconf_parent']
-    # Sort the sections by name
-    sections.sort(key=lambda x:x[0])
-    return sections
-
-
-def partition_instance(obj):
-    """Return scalars,sections for a given Sconf instance.
-    """
-    scnames = []
-    sections = []
-    for k,v in obj.__dict__.iteritems():
-        if isinstance(v,SConfig):
-            if not k=='_sconf_parent':
-                sections.append((k,v))
-        else:
-            scnames.append(k)
-
-    # Sort the sections by name
-    sections.sort(key=lambda x:x[0])
-
-    # Sort the scalar names, filter them and then extract the actual objects
-    scnames.sort()
-    scnames = filter_scalars(scnames)
-    scalars = [(s,obj.__dict__[s]) for s in scnames]
-    
-    return scalars, sections
-
-
-def mk_ConfigObj(filename,mk_missing_file=True):
-    """Return a ConfigObj instance with our hardcoded conventions.
-
-    Use a simple factory that wraps our option choices for using ConfigObj.
-    I'm hard-wiring certain choices here, so we'll always use instances with
-    THESE choices.
-
-    :Parameters:
-
-      filename : string
-        File to read from.
-
-    :Keywords:
-      makeMissingFile : bool (True)
-        If true, the file named by `filename` may not yet exist and it will be
-        automatically created (empty).  Else, if `filename` doesn't exist, an
-        IOError will be raised.
-    """
-
-    if mk_missing_file:
-        create_empty = True
-        file_error = False
-    else:
-        create_empty = False
-        file_error = True
-        
-    return configobj.ConfigObj(filename,
-                               create_empty=create_empty,
-                               file_error=file_error,
-                               indent_type='    ',
-                               interpolation='Template',
-                               unrepr=True)
-
-nullConf = mk_ConfigObj(None)
-
-
-class RecursiveConfigObj(object):
-    """Object-oriented interface for recursive ConfigObj constructions."""
-
-    def __init__(self,filename):
-        """Return a ConfigObj instance with our hardcoded conventions.
-
-        Use a simple factory that wraps our option choices for using ConfigObj.
-        I'm hard-wiring certain choices here, so we'll always use instances with
-        THESE choices.
-
-        :Parameters:
-
-          filename : string
-            File to read from.
-        """
-
-        self.comp = []
-        self.conf = self._load(filename)
-
-    def _load(self,filename,mk_missing_file=True):
-        conf = mk_ConfigObj(filename,mk_missing_file)
-
-        # Do recursive loading. We only allow (or at least honor) the include
-        # tag at the top-level.  For now, we drop the inclusion information so
-        # that there are no restrictions on which levels of the SConfig
-        # hierarchy can use include statements.  But this means that
-
-        # if bookkeeping of each separate component of the recursive
-        # construction was requested, make a separate object for storage
-        # there, since we don't want that to be modified by the inclusion
-        # process.
-        self.comp.append(mk_ConfigObj(filename,mk_missing_file))
-
-        incfname = conf.pop('include',None)
-        if incfname is not None:
-            # Do recursive load.  We don't want user includes that point to
-            # missing files to fail silently, so in the recursion we disable
-            # auto-creation of missing files.
-            confinc = self._load(incfname,mk_missing_file=False)
-
-            # Update with self to get proper ordering (included files provide
-            # base data, current one overwrites)
-            confinc.update(conf)
-            # And do swap to return the updated structure
-            conf = confinc
-            # Set the filename to be the original file instead of the included
-            # one
-            conf.filename = filename
-        return conf
-        
-############################################################################
-# Main SConfig class and supporting exceptions
-############################################################################
-
-class SConfigError(Exception): pass
-
-class SConfigInvalidKeyError(SConfigError): pass
-
-class SConfig(object):
-    """A class representing configuration objects.
-
-    Note: this class should NOT have any traits itself, since the actual traits
-    will be declared by subclasses.  This class is meant to ONLY declare the
-    necessary initialization/validation methods.  """
-
-    # Any traits declared here are prefixed with _sconf_ so that our special
-    # formatting/analysis utilities can distinguish them from user traits and
-    # can avoid them.
-    
-    # Once created, the tree's hierarchy can NOT be modified
-    _sconf_parent = None
-
-    def __init__(self,config=None,parent=None,monitor=None):
-        """Makes an  SConfig object out of a ConfigObj instance
-        """
-
-        if config is None:
-            config = mk_ConfigObj(None)
-
-        # Validate the set of scalars ...
-        my_scalars = set(get_scalars(self))
-        cf_scalars = set(config.scalars)
-        invalid_scalars = cf_scalars - my_scalars
-        if invalid_scalars:
-            config_fname = get_config_filename(config)
-            m=("In config defined in file: %r\n"
-               "Error processing section: %s\n"
-               "These keys are invalid : %s\n"
-               "Valid key names        : %s\n"
-               % (config_fname,self.__class__.__name__,
-                  list(invalid_scalars),list(my_scalars)))
-            raise SConfigInvalidKeyError(m)
-
-        # ... and sections
-        section_items = get_sections(self.__class__,SConfig)
-        my_sections = set([n for n,v in section_items])
-        cf_sections = set(config.sections)
-        invalid_sections = cf_sections - my_sections
-        if invalid_sections:
-            config_fname = get_config_filename(config)
-            m = ("In config defined in file: %r\n"
-                 "Error processing section: %s\n"
-                 "These subsections are invalid : %s\n"
-                 "Valid subsection names        : %s\n"
-                 % (config_fname,self.__class__.__name__,
-                    list(invalid_sections),list(my_sections)))
-            raise SConfigInvalidKeyError(m)
-
-        self._sconf_parent = parent
-
-        # Now set the traits based on the config
-        for k in my_scalars:
-            setattr(self,k,config[k])
-
-        # And build subsections
-        for s,v in section_items:
-            sec_config = config.setdefault(s,{})
-            section = v(sec_config,self,monitor=monitor)
-
-            # We must use add_trait instead of setattr because we inherit from
-            # HasStrictTraits, but we need to then do a 'dummy' getattr call on
-            # self so the class trait propagates to the instance.
-            self.add_trait(s,section)
-            getattr(self,s)
-
-    def __repr__(self,depth=0):
-        """Dump a section to a string."""
-
-        indent = '    '*(depth)
-
-        top_name = self.__class__.__name__
-
-        if depth == 0:
-            label = '# %s - plaintext (in .conf format)\n' % top_name
-        else:
-            # Section titles are indented one level less than their contents in
-            # the ConfigObj write methods.
-            sec_indent = '    '*(depth-1)
-            label = '\n'+sec_indent+('[' * depth) + top_name + (']'*depth)
-
-        out = [label]
-
-        doc = self.__class__.__doc__
-        if doc is not None:
-            out.append(comment(dedent(doc),indent))
-
-        scalars, sections = partition_instance(self)
-
-        for s,v in scalars:
-            try:
-                info = self.__base_traits__[s].handler.info()
-                # Get a short version of info with lines of max. 78 chars, so
-                # that after commenting them out (with '# ') they are at most
-                # 80-chars long.
-                out.append(comment(wrap('',info.replace('\n', ' '),78-len(indent)),indent))
-            except (KeyError,AttributeError):
-                pass
-            out.append(indent+('%s = %r' % (s,v)))
-
-        for sname,sec in sections:
-            out.append(sec.__repr__(depth+1))
-
-        return '\n'.join(out)
-
-    def __str__(self):
-        return self.__class__.__name__
-
-
-##############################################################################
-# High-level class(es) and utilities for handling a coupled pair of SConfig and
-# ConfigObj instances.
-##############################################################################
-
-def path_to_root(obj):
-    """Find the path to the root of a nested SConfig instance."""
-    ob = obj
-    path = []
-    while ob._sconf_parent is not None:
-        path.append(ob.__class__.__name__)
-        ob = ob._sconf_parent
-    path.reverse()
-    return path
-
-
-def set_value(fconf,path,key,value):
-    """Set a value on a ConfigObj instance, arbitrarily deep."""
-    section = fconf
-    for sname in path:
-        section = section.setdefault(sname,{})
-    section[key] = value
-
-
-def fmonitor(fconf):
-    """Make a monitor for coupling SConfig instances to ConfigObj ones.
-
-    We must use a closure because Traits makes assumptions about the functions
-    used with on_trait_change() that prevent the use of a callable instance.
-    """
-    
-    def mon(obj,name,new):
-        #print 'OBJ:',obj  # dbg
-        #print 'NAM:',name # dbg
-        #print 'NEW:',new  # dbg
-        set_value(fconf,path_to_root(obj),name,new)
-        
-    return mon
-
-
-class SConfigManager(object):
-    """A simple object to manage and sync a SConfig and a ConfigObj pair.
-    """
-    
-    def __init__(self,configClass,configFilename,filePriority=True):
-        """Make a new SConfigManager.
-
-        :Parameters:
-        
-          configClass : class
-
-          configFilename : string
-            If the filename points to a non-existent file, it will be created
-            empty.  This is useful when creating a file form from an existing
-            configClass with the class defaults.
-
-
-        :Keywords:
-
-          filePriority : bool (True)
-
-            If true, at construction time the file object takes priority and
-            overwrites the contents of the config object.  Else, the data flow
-            is reversed and the file object will be overwritten with the
-            configClass defaults at write() time.
-        """
-
-        rconf = RecursiveConfigObj(configFilename)
-        # In a hierarchical object, the two following fconfs are *very*
-        # different.  In self.fconf, we'll keep the outer-most fconf associated
-        # directly to the original filename.  self.fconf_combined, instead,
-        # contains an object which has the combined effect of having merged all
-        # the called files in the recursive chain.
-        self.fconf = rconf.comp[0]
-        self.fconf_combined = rconf.conf
-
-        # Create a monitor to track and apply trait changes to the sconf
-        # instance over into the fconf one
-        monitor = fmonitor(self.fconf)
-        
-        if filePriority:
-            self.sconf = configClass(self.fconf_combined,monitor=monitor)
-        else:
-            # Push defaults onto file object
-            self.sconf = configClass(mk_ConfigObj(None),monitor=monitor)
-            self.fconfUpdate(self.fconf,self.sconf)
-
-    def fconfUpdate(self,fconf,sconf):
-        """Update the fconf object with the data from sconf"""
-
-        scalars, sections = partition_instance(sconf)
-
-        for s,v in scalars:
-            fconf[s] = v
-
-        for secname,sec in sections:
-            self.fconfUpdate(fconf.setdefault(secname,{}),sec)
-
-    def write(self,filename=None):
-        """Write out to disk.
-
-        This method writes out only to the top file in a hierarchical
-        configuration, which means that the class defaults and other values not
-        explicitly set in the top level file are NOT written out.
-
-        :Keywords:
-        
-          filename : string (None)
-            If given, the output is written to this file, otherwise the
-            .filename attribute of the top-level configuration object is used.
-        """
-        if filename is not None:
-            file_obj = open(filename,'w')
-            out = self.fconf.write(file_obj)
-            file_obj.close()
-            return out
-        else:
-            return self.fconf.write()
-
-    def writeAll(self,filename=None):
-        """Write out the entire configuration to disk.
-
-        This method, in contrast with write(), updates the .fconf_combined
-        object with the *entire* .sconf instance, and then writes it out to
-        disk.  This method is thus useful for generating files that have a
-        self-contained, non-hierarchical file.
-
-        :Keywords:
-        
-          filename : string (None)
-            If given, the output is written to this file, otherwise the
-            .filename attribute of the top-level configuration object is used.
-        """
-        if filename is not None:
-            file_obj = open(filename,'w')
-            self.fconfUpdate(self.fconf_combined,self.sconf)
-            out = self.fconf_combined.write(file_obj)
-            file_obj.close()
-            return out
-        else:
-            self.fconfUpdate(self.fconf_combined,self.sconf)
-            return self.fconf_combined.write()
-
-    def sconf_str(self):
-        return str(self.sconf)
-
-    def fconf_str(self):
-        return configobj2str(self.fconf)
-
-    __repr__ = __str__ = fconf_str
diff --git a/IPython/config/tests/sample_config.py b/IPython/config/tests/sample_config.py
new file mode 100644
index 0000000..0efddfe
--- /dev/null
+++ b/IPython/config/tests/sample_config.py
@@ -0,0 +1,19 @@
+from IPython.config.api import *
+
+class SubSubSample(Config):
+    my_int = Int(3)
+
+
+class Sample(Config):
+    my_float = Float(3)
+    my_choice = Enum('a','b','c')
+
+    class MiddleSection(Config):
+        left_alone = Enum('1','2','c')
+        unknown_mod = Module('asd')
+
+    class SubSample(Config):
+        subsample_uri = URI('http://localhost:8080')
+
+        # Example of how to include external config
+        SubSubSample = SubSubSample()
diff --git a/IPython/config/tests/sctst.py b/IPython/config/tests/sctst.py
deleted file mode 100644
index 7769d08..0000000
--- a/IPython/config/tests/sctst.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Little utilities for testing tconfig.
-
-This module is meant to be used via
-
-import sctst; reload(sctst)
-from sctst import *
-
-at the top of the actual test scripts, so that they all get the entire set of
-common test tools with minimal fuss.
-"""
-
-# Standard library imports
-import os
-import sys
-from pprint import pprint
-
-# Our own imports.
-
-from IPython.config import sconfig
-reload(sconfig)
-
-from sconfig import mkConfigObj, RecursiveConfigObj, SConfigManager, \
-     sconf2file
-
-# Simple utilities/classes for testing
-
-def cat(fname):
-    print '### FILENAME:',fname
-    print open(fname).read()
-
-
-class App(object):
-    """A trivial 'application' class to be initialized.
-    """
-    def __init__(self,config_class,config_filename):
-        self.rcman = SConfigManager(config_class,config_filename)
-        self.rc = self.rcman.sconf
diff --git a/IPython/config/tests/simple.conf b/IPython/config/tests/simple.conf
deleted file mode 100644
index ba0ca67..0000000
--- a/IPython/config/tests/simple.conf
+++ /dev/null
@@ -1,14 +0,0 @@
-# Toy example of a TConfig-based configuration description
-
-# This is the class declaration for the configuration:
-
-#  SimpleConfig
-#  Configuration for my application
-
-solver = "Iterative2"
-
-[Protocol]
-    # Specify the Protocol
-
-    ptype = "http2"
-    max_users = 4 
diff --git a/IPython/config/tests/simple.spec.conf b/IPython/config/tests/simple.spec.conf
deleted file mode 100644
index 1fcb9e1..0000000
--- a/IPython/config/tests/simple.spec.conf
+++ /dev/null
@@ -1,14 +0,0 @@
-# Toy example of a TConfig-based configuration description
-
-# This is the class declaration for the configuration:
-
-#  SimpleConfig
-#  Configuration for my application
-
-datafile = string(default='data.txt')
-solver = option('Direct','Iterative')
-
-[Protocol]
-    # Specify the Protocol
-    ptype = option('http','ftp','ssh')
-    max_users = integer(1,10)
diff --git a/IPython/config/tests/simpleconf.py b/IPython/config/tests/simpleconf.py
deleted file mode 100644
index 2af9794..0000000
--- a/IPython/config/tests/simpleconf.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""Toy example of reading an SConf object."""
-
-from IPython.external.configobj import ConfigObj
-from IPython.external import configobj, validate
-
-
-from IPython.config import sconfig
-reload(sconfig)
-
-configspecfilename = 'simple.spec.conf'
-filename = 'simple.conf'
-
-print '*'*80
-configspec = ConfigObj(configspecfilename, encoding='UTF8',
-                       list_values=False)
-print sconfig.configobj2str(configspec)
-
-print '*'*80
-config = ConfigObj(filename, configspec=configspec,
-                   interpolation='Template',
-                   unrepr=True)
-print sconfig.configobj2str(config)
-vdt = validate.Validator()
-test = config.validate(vdt,preserve_errors=True)
-
-####
-vdt = validate.Validator()
-class Bunch: pass
-vf = Bunch()
-vf.__dict__.update(vdt.functions)
-vf.pass_ = vdt.functions['pass']
-vf.__dict__.pop('',None)
-vf.__dict__.pop('pass',None)
-###
-
-
-if test==True:
-    print 'All OK'
-else:
-    err = configobj.flatten_errors(config,test)
-    print 'Flat errors:'
-    for secs,key,result in err:
-        if secs == []:
-            print 'DEFAULT:','key:',key,'err:',result
-        else:
-            print 'Secs:',secs,'key:',key,'err:',result
-
-
-##
-print '*'*80
-
-sc = sconfig.SConfig(configspecfilename)
-
-
-
-####
-
-            
-        
diff --git a/IPython/config/tests/test_config.py b/IPython/config/tests/test_config.py
new file mode 100644
index 0000000..27ab192
--- /dev/null
+++ b/IPython/config/tests/test_config.py
@@ -0,0 +1,135 @@
+"""
+# Test utilities
+
+>>> import os
+
+>>> def dict_as_sorted_list(d):
+...     for k in d:
+...         if isinstance(d[k],dict):
+...             d[k] = dict_as_sorted_list(d[k])
+...     return sorted(d.items())
+
+>>> def pprint(d,level=0):
+...     if isinstance(d,dict):
+...         d = dict_as_sorted_list(d)
+...     for item,value in d:
+...         if isinstance(value,list):
+...             print "%s%s" % (' '*level, item)
+...             pprint(value,level+2)
+...         else:
+...             print "%s%s: %s" % (' '*level, item, value)
+
+
+# Tests
+
+>>> from IPython.config.api import *
+>>> from sample_config import *
+
+>>> s = Sample()
+>>> print s.my_float
+3.0
+>>> s.my_float = 4
+>>> print s.my_float
+4.0
+>>> print type(s.my_float)
+<type 'float'>
+>>> s.SubSample.SubSubSample.my_int = 5.0
+>>> print s.SubSample.SubSubSample.my_int
+5
+
+>>> i = ConfigInspector(s)
+>>> print i.properties
+[('my_choice', 'a'), ('my_float', 4.0)]
+>>> print tuple(s for s,v in i.subconfigs)
+('MiddleSection', 'SubSample')
+
+>>> print s
+my_float = 4.0
+<BLANKLINE>
+[SubSample]
+<BLANKLINE>
+  [[SubSubSample]]
+    my_int = 5
+<BLANKLINE>
+
+>>> import tempfile
+>>> fn = tempfile.mktemp()
+>>> f = open(fn,'w')
+>>> f.write(str(s))
+>>> f.close()
+
+>>> s += fn
+
+>>> from IPython.external.configobj import ConfigObj
+>>> c = ConfigObj(fn)
+>>> c['SubSample']['subsample_uri'] = 'http://ipython.scipy.org'
+
+>>> s += c
+>>> print s
+my_float = 4.0
+<BLANKLINE>
+[SubSample]
+  subsample_uri = 'http://ipython.scipy.org'
+<BLANKLINE>
+  [[SubSubSample]]
+    my_int = 5
+<BLANKLINE>
+
+>>> pprint(dict_from_config(s,only_modified=False))
+MiddleSection
+  left_alone: '1'
+  unknown_mod: 'asd'
+SubSample
+  SubSubSample
+    my_int: 5
+  subsample_uri: 'http://ipython.scipy.org'
+my_choice: 'a'
+my_float: 4.0
+
+>>> pprint(dict_from_config(s))
+SubSample
+  SubSubSample
+    my_int: 5
+  subsample_uri: 'http://ipython.scipy.org'
+my_float: 4.0
+
+Test roundtripping:
+
+>>> fn = tempfile.mktemp()
+>>> f = open(fn, 'w')
+>>> f.write('''
+... [MiddleSection]
+... # some comment here
+... left_alone = 'c'
+... ''')
+>>> f.close()
+
+>>> s += fn
+
+>>> pprint(dict_from_config(s))
+MiddleSection
+  left_alone: 'c'
+SubSample
+  SubSubSample
+    my_int: 5
+  subsample_uri: 'http://ipython.scipy.org'
+my_float: 4.0
+
+>>> write(s, fn)
+>>> f = file(fn,'r')
+>>> ConfigInspector(s).reset()
+>>> pprint(dict_from_config(s))
+
+>>> s += fn
+>>> os.unlink(fn)
+>>> pprint(dict_from_config(s))
+MiddleSection
+  left_alone: 'c'
+SubSample
+  SubSubSample
+    my_int: 5
+  subsample_uri: 'http://ipython.scipy.org'
+my_float: 4.0
+
+
+"""
diff --git a/IPython/config/traitlets.py b/IPython/config/traitlets.py
new file mode 100644
index 0000000..f1720c5
--- /dev/null
+++ b/IPython/config/traitlets.py
@@ -0,0 +1,322 @@
+"""Traitlets -- a light-weight meta-class free stand-in for Traits.
+
+Traitlet behaviour
+==================
+- Automatic casting, equivalent to traits.C* classes, e.g. CFloat, CBool etc.
+
+- By default, validation is done by attempting to cast a given value
+  to the underlying type, e.g. bool for Bool, float for Float etc.
+
+- To set or get a Traitlet value, use the ()-operator.  E.g.
+
+  >>> b = Bool(False)
+  >>> b(True)
+  >>> print b # returns a string representation of the Traitlet
+  True
+  >>> print b() # returns the underlying bool object
+  True
+
+  This makes it possible to change values "in-place", unlike an assigment
+  of the form
+
+  >>> c = Bool(False)
+  >>> c = True
+
+  which results in
+
+  >>> print type(b), type(c)
+  <class 'IPython.config.traitlets.Bool'> <type 'bool'>
+
+- Each Traitlet keeps track of its modification state, e.g.
+
+  >>> c = Bool(False)
+  >>> print c.modified
+  False
+  >>> c(False)
+  >>> print c.modified
+  False
+  >>> c(True)
+  >>> print c.modified
+  True
+
+How to customize Traitlets
+==========================
+
+The easiest way to create a new Traitlet is by wrapping an underlying
+Python type.  This is done by setting the "_type" class attribute.  For
+example, creating an int-like Traitlet is done as follows:
+
+>>> class MyInt(Traitlet):
+...     _type = int
+
+>>> i = MyInt(3)
+>>> i(4)
+>>> print i
+4
+
+>>> try:
+...     i('a')
+... except ValidationError:
+...    pass # this is expected
+... else:
+...     "This should not be reached."
+
+Furthermore, the following methods are provided for finer grained
+control of validation and assignment:
+
+ - validate(self,value)
+   Ensure that "value" is valid.  If not, raise an exception of any kind
+   with a suitable error message, which is reported to the user.
+
+ - prepare_value(self)
+   When using the ()-operator to query the underlying Traitlet value,
+   that value is first passed through prepare_value.  For example:
+
+   >>> class Eval(Traitlet):
+   ...     _type = str
+   ...
+   ...     def prepare_value(self):
+   ...         return eval(self._value)
+
+   >>> x = Eval('1+1')
+   >>> print x
+   '1+1'
+   >>> print x()
+   2
+
+ - __repr__(self)
+   By default, repr(self._value) is returned.  This can be customised
+   to, for example, first call prepare_value and return the repr of
+   the resulting object.
+
+"""
+
+import re
+import types
+
+class ValidationError(Exception):
+    pass
+
+class Traitlet(object):
+    """Traitlet which knows its modification state.
+
+    """
+    def __init__(self, value):
+        "Validate and store the default value.  State is 'unmodified'."
+        self._type = getattr(self,'_type',None)
+        value = self._parse_validation(value)
+        self._default_value = value
+        self.reset()
+
+    def reset(self):
+        self._value = self._default_value
+        self._changed = False
+
+    def validate(self, value):
+        "Validate the given value."
+        if self._type is not None:
+            self._type(value)
+
+    def _parse_validation(self, value):
+        """Run validation and return a descriptive error if needed.
+
+        """
+        try:
+            self.validate(value)
+        except Exception, e:
+            err_message = 'Cannot convert "%s" to %s' % \
+                          (value, self.__class__.__name__.lower())
+            if e.message:
+                err_message += ': %s' % e.message
+            raise ValidationError(err_message)
+        else:
+            # Cast to appropriate type before storing
+            if self._type is not None:
+                value = self._type(value)
+            return value
+
+    def prepare_value(self):
+        """Run on value before it is ever returned to the user.
+
+        """
+        return self._value
+
+    def __call__(self,value=None):
+        """Query or set value depending on whether `value` is specified.
+
+        """
+        if value is None:
+            return self.prepare_value()
+
+        self._value = self._parse_validation(value)
+        self._changed = (self._value != self._default_value)
+
+    @property
+    def modified(self):
+        "Whether value has changed from original definition."
+        return self._changed
+
+    def __repr__(self):
+        """This class is represented by the underlying repr.  Used when
+        dumping value to file.
+
+        """
+        return repr(self._value)
+
+class Float(Traitlet):
+    """
+    >>> f = Float(0)
+    >>> print f.modified
+    False
+
+    >>> f(3)
+    >>> print f()
+    3.0
+    >>> print f.modified
+    True
+
+    >>> f(0)
+    >>> print f()
+    0.0
+    >>> print f.modified
+    False
+
+    >>> try:
+    ...    f('a')
+    ... except ValidationError:
+    ...    pass
+
+    """
+    _type = float
+
+class Enum(Traitlet):
+    """
+    >>> c = Enum('a','b','c')
+    >>> print c()
+    a
+
+    >>> try:
+    ...    c('unknown')
+    ... except ValidationError:
+    ...    pass
+
+    >>> print c.modified
+    False
+
+    >>> c('b')
+    >>> print c()
+    b
+
+    """
+    def __init__(self, *options):
+        self._options = options
+        super(Enum,self).__init__(options[0])
+
+    def validate(self, value):
+        if not value in self._options:
+            raise ValueError('must be one of %s' % str(self._options))
+
+class Module(Traitlet):
+    """
+    >>> m = Module('some.unknown.module')
+    >>> print m
+    'some.unknown.module'
+
+    >>> m = Module('re')
+    >>> assert type(m()) is types.ModuleType
+
+    """
+    _type = str
+
+    def prepare_value(self):
+        try:
+            module = eval(self._value)
+        except:
+            module = None
+
+        if type(module) is not types.ModuleType:
+            raise ValueError("Invalid module name: %s" % self._value)
+        else:
+            return module
+
+
+class URI(Traitlet):
+    """
+    >>> u = URI('http://')
+
+    >>> try:
+    ...    u = URI('something.else')
+    ... except ValidationError:
+    ...    pass
+
+    >>> u = URI('http://ipython.scipy.org/')
+    >>> print u
+    'http://ipython.scipy.org/'
+
+    """
+    _regexp = re.compile(r'^[a-zA-Z]+:\/\/')
+    _type = str
+
+    def validate(self, uri):
+        if not self._regexp.match(uri):
+            raise ValueError()
+
+class Int(Traitlet):
+    """
+    >>> i = Int(3.5)
+    >>> print i
+    3
+    >>> print i()
+    3
+
+    >>> i = Int('4')
+    >>> print i
+    4
+
+    >>> try:
+    ...    i = Int('a')
+    ... except ValidationError:
+    ...    pass
+    ... else:
+    ...    raise "Should fail"
+
+    """
+    _type = int
+
+class Bool(Traitlet):
+    """
+    >>> b = Bool(2)
+    >>> print b
+    True
+    >>> print b()
+    True
+
+    >>> b = Bool('True')
+    >>> print b
+    True
+    >>> b(True)
+    >>> print b.modified
+    False
+
+    >>> print Bool(0)
+    False
+
+    """
+    _type = bool
+
+class Unicode(Traitlet):
+    """
+    >>> u = Unicode(123)
+    >>> print u
+    u'123'
+
+    >>> u = Unicode('123')
+    >>> print u.modified
+    False
+
+    >>> u('hello world')
+    >>> print u
+    u'hello world'
+
+    """
+    _type = unicode