From 4999878da9f7dd1e1bb4931c831c387deaf7f11c 2011-04-24 00:42:02
From: Brian Granger <ellisonbg@gmail.com>
Date: 2011-04-24 00:42:02
Subject: [PATCH] Ongoing work on the config system.

At this point, I am mostly testing out various approaches, but:

* class_traits and class_trait_names added.
* appconfig example created for testing new config ideas.
* InteractiveShell traits updated.
* shortname handling added to command line config loader.

---

diff --git a/IPython/config/configurable.py b/IPython/config/configurable.py
index ed8fc2a..12abd1c 100755
--- a/IPython/config/configurable.py
+++ b/IPython/config/configurable.py
@@ -27,6 +27,7 @@ from weakref import WeakValueDictionary
 from IPython.utils.importstring import import_item
 from loader import Config
 from IPython.utils.traitlets import HasTraits, Instance
+from IPython.utils.text import indent
 
 
 #-----------------------------------------------------------------------------
@@ -137,3 +138,33 @@ class Configurable(HasTraits):
                         # shared by all instances, effectively making it a class attribute.
                         setattr(self, k, deepcopy(config_value))
 
+    @classmethod
+    def class_get_shortnames(cls):
+        """Return the shortname to fullname dict for config=True traits."""
+        cls_traits = cls.class_traits(config=True)
+        shortnames = {}
+        for k, v in cls_traits.items():
+            shortname = v.get_metadata('shortname')
+            if shortname is not None:
+                longname = cls.__name__ + '.' + k
+                shortnames[shortname] = longname
+        return shortnames
+
+    @classmethod
+    def class_get_help(cls):
+        cls_traits = cls.class_traits(config=True)
+        final_help = []
+        final_help.append('%s options' % cls.__name__)
+        final_help.append(len(final_help[0])*'-')
+        for k, v in cls_traits.items():
+            help = v.get_metadata('help')
+            final_help.append(k + " : " + v.__class__.__name__)
+            if help is not None:
+                final_help.append(indent(help))
+        return '\n'.join(final_help)
+
+    @classmethod
+    def class_print_help(cls):
+        print cls.class_get_help()
+
+            
\ No newline at end of file
diff --git a/IPython/config/loader.py b/IPython/config/loader.py
index 6a0b3e6..720aef5 100644
--- a/IPython/config/loader.py
+++ b/IPython/config/loader.py
@@ -315,7 +315,7 @@ class KeyValueConfigLoader(CommandLineConfigLoader):
         ipython Global.profile="foo" InteractiveShell.autocall=False
     """
 
-    def __init__(self, argv=None):
+    def __init__(self, argv=None, classes=None):
         """Create a key value pair config loader.
 
         Parameters
@@ -324,6 +324,9 @@ class KeyValueConfigLoader(CommandLineConfigLoader):
             A list that has the form of sys.argv[1:] which has unicode
             elements of the form u"key=value". If this is None (default),
             then sys.argv[1:] will be used.
+        class : (list, tuple) of Configurables
+            A sequence of Configurable classes that will be used to map
+            shortnames to longnames.
 
         Returns
         -------
@@ -338,11 +341,14 @@ class KeyValueConfigLoader(CommandLineConfigLoader):
             >>> cl.load_config(["foo='bar'","A.name='brian'","B.number=0"])
             {'A': {'name': 'brian'}, 'B': {'number': 0}, 'foo': 'bar'}
         """
-        if argv == None:
+        if argv is None:
             argv = sys.argv[1:]
+        if classes is None:
+            classes = () 
         self.argv = argv
+        self.classes = classes
 
-    def load_config(self, argv=None):
+    def load_config(self, argv=None, classes=None):
         """Parse the configuration and generate the Config object.
 
         Parameters
@@ -350,16 +356,47 @@ class KeyValueConfigLoader(CommandLineConfigLoader):
         argv : list, optional
             A list that has the form of sys.argv[1:] which has unicode
             elements of the form u"key=value". If this is None (default),
-            then self.argv will be used. 
+            then self.argv will be used.
+        class : (list, tuple) of Configurables
+            A sequence of Configurable classes that will be used to map
+            shortnames to longnames.
         """
         self.clear()
         if argv is None:
             argv = self.argv
+        if classes is None:
+            classes = self.classes
+
+        # print argv
+
+        shortnames = {}
+        for cls in classes:
+            sn = cls.class_get_shortnames()
+            # Check for duplicate shortnames and raise if found.
+            for k, v in sn.items():
+                if k in shortnames:
+                    raise KeyError(
+                        "duplicate shortname: %s and %s both use the shortname: %s" %\
+                        (v, shortnames[k], k)
+                    )
+            shortnames.update(sn)
+
+        # print shortnames
+
         for item in argv:
             pair = tuple(item.split("="))
             if len(pair) == 2:
-                exec_str = 'self.config.' + pair[0] + '=' + pair[1]
-                exec exec_str in locals(), globals()
+                lhs = pair[0]
+                rhs = pair[1]
+                # Substitute longnames for shortnames.
+                if lhs in shortnames:
+                    lhs = shortnames[lhs]
+                exec_str = 'self.config.' + lhs + '=' + rhs
+                try:
+                    exec exec_str in locals(), globals()
+                except (NameError, SyntaxError):
+                    exec_str = 'self.config.' + lhs + '="' + rhs + '"'
+                    exec exec_str in locals(), globals()
         return self.config
 
 
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
index 4f4e5f6..3ba7de4 100644
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -214,7 +214,7 @@ class InteractiveShell(Configurable, Magic):
     # We can't do this yet because even runlines uses the autoindent.
     autoindent = CBool(True, config=True, help=
         """
-        Should IPython indent code entered interactively.
+        Autoindent IPython code entered interactively.
         """
     )
     automagic = CBool(True, config=True, help=
@@ -222,12 +222,37 @@ class InteractiveShell(Configurable, Magic):
         Enable magic commands to be called without the leading %.
         """
     )
-    cache_size = Int(1000, config=True)
-    color_info = CBool(True, config=True)
+    cache_size = Int(1000, config=True, help=
+        """
+        Set the size of the output cache.  The default is 1000, you can
+        change it permanently in your config file.  Setting it to 0 completely
+        disables the caching system, and the minimum value accepted is 20 (if
+        you provide a value less than 20, it is reset to 0 and a warning is
+        issued).  This limit is defined because otherwise you'll spend more
+        time re-flushing a too small cache than working
+        """
+    )
+    color_info = CBool(True, config=True, help=
+        """
+        Use colors for displaying information about objects. Because this
+        information is passed through a pager (like 'less'), and some pagers
+        get confused with color codes, this capability can be turned off.
+        """
+    )
     colors = CaselessStrEnum(('NoColor','LightBG','Linux'), 
                              default_value=get_default_colors(), config=True)
     debug = CBool(False, config=True)
-    deep_reload = CBool(False, config=True)
+    deep_reload = CBool(False, config=True, help=
+        """
+        Enable deep (recursive) reloading by default. IPython can use the
+        deep_reload module which reloads changes in modules recursively (it
+        replaces the reload() function, so you don't need to change anything to
+        use it). deep_reload() forces a full reload of modules whose code may
+        have changed, which the default reload() function does not.  When
+        deep_reload is off, IPython will use the normal reload(), but
+        deep_reload will still be available as dreload().
+        """
+    )
     display_formatter = Instance(DisplayFormatter)
     displayhook_class = Type(DisplayHook)
     display_pub_class = Type(DisplayPublisher)
@@ -245,12 +270,28 @@ class InteractiveShell(Configurable, Magic):
     # interactive statements or whole blocks.
     input_splitter = Instance('IPython.core.inputsplitter.IPythonInputSplitter',
                               (), {})
-    logstart = CBool(False, config=True)
-    logfile = Unicode('', config=True)
-    logappend = Unicode('', config=True)
+    logstart = CBool(False, config=True, help=
+        """
+        Start logging to the default log file.
+        """
+    )
+    logfile = Unicode('', config=True, help=
+        """
+        The name of the logfile to use.
+        """
+    )
+    logappend = Unicode('', config=True, help=
+        """
+        Start logging to the given file in append mode.
+        """
+    )
     object_info_string_level = Enum((0,1,2), default_value=0,
                                     config=True)
-    pdb = CBool(False, config=True)
+    pdb = CBool(False, config=True, help=
+        """
+        Automatically call the pdb debugger after every exception.
+        """
+    )
 
     profile = Unicode('', config=True)
     prompt_in1 = Str('In [\\#]: ', config=True)
diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py
index 724d631..ca30c84 100755
--- a/IPython/utils/tests/test_traitlets.py
+++ b/IPython/utils/tests/test_traitlets.py
@@ -366,6 +366,7 @@ class TestHasTraits(TestCase):
             f = Float
         a = A()
         self.assertEquals(a.trait_names(),['i','f'])
+        self.assertEquals(A.class_trait_names(),['i','f'])
 
     def test_trait_metadata(self):
         class A(HasTraits):
@@ -379,6 +380,7 @@ class TestHasTraits(TestCase):
             f = Float
         a = A()
         self.assertEquals(a.traits(), dict(i=A.i, f=A.f))
+        self.assertEquals(A.class_traits(), dict(i=A.i, f=A.f))
 
     def test_traits_metadata(self):
         class A(HasTraits):
diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py
index f570c62..21187cc 100644
--- a/IPython/utils/traitlets.py
+++ b/IPython/utils/traitlets.py
@@ -515,6 +515,48 @@ class HasTraits(object):
             for n in names:
                 self._add_notifiers(handler, n)
 
+    @classmethod
+    def class_trait_names(cls, **metadata):
+        """Get a list of all the names of this classes traits.
+
+        This method is just like the :meth:`trait_names` method, but is unbound.
+        """
+        return cls.class_traits(**metadata).keys()
+
+    @classmethod
+    def class_traits(cls, **metadata):
+        """Get a list of all the traits of this class.
+
+        This method is just like the :meth:`traits` method, but is unbound.
+
+        The TraitTypes returned don't know anything about the values
+        that the various HasTrait's instances are holding.
+
+        This follows the same algorithm as traits does and does not allow
+        for any simple way of specifying merely that a metadata name
+        exists, but has any value.  This is because get_metadata returns
+        None if a metadata key doesn't exist.
+        """
+        traits = dict([memb for memb in getmembers(cls) if \
+                     isinstance(memb[1], TraitType)])
+
+        if len(metadata) == 0:
+            return traits
+
+        for meta_name, meta_eval in metadata.items():
+            if type(meta_eval) is not FunctionType:
+                metadata[meta_name] = _SimpleTest(meta_eval)
+
+        result = {}
+        for name, trait in traits.items():
+            for meta_name, meta_eval in metadata.items():
+                if not meta_eval(trait.get_metadata(meta_name)):
+                    break
+            else:
+                result[name] = trait
+
+        return result
+
     def trait_names(self, **metadata):
         """Get a list of all the names of this classes traits."""
         return self.traits(**metadata).keys()
diff --git a/docs/examples/core/appconfig.py b/docs/examples/core/appconfig.py
new file mode 100644
index 0000000..28644ce
--- /dev/null
+++ b/docs/examples/core/appconfig.py
@@ -0,0 +1,55 @@
+import sys
+
+from IPython.config.configurable import Configurable
+from IPython.utils.traitlets import (
+    Bool, Unicode, Int, Float, List
+)
+from IPython.config.loader import KeyValueConfigLoader
+
+class Foo(Configurable):
+
+    i = Int(0, config=True, shortname='i', help="The integer i.")
+    j = Int(1, config=True, shortname='j', help="The integer j.")
+    name = Unicode(u'Brian', config=True, shortname='name', help="First name.")
+
+
+class Bar(Configurable):
+
+    enabled = Bool(True, config=True, shortname="bar-enabled", help="Enable bar.")
+
+
+class MyApp(Configurable):
+
+    app_name = Unicode(u'myapp', config=True, shortname="myapp", help="The app name.")
+    running = Bool(False, config=True, shortname="running", help="Is the app running?")
+    classes = List([Bar, Foo])
+
+    def __init__(self, **kwargs):
+        Configurable.__init__(self, **kwargs)
+        self.classes.insert(0, self.__class__)
+
+    def print_help(self):
+        for cls in self.classes:
+            cls.class_print_help()
+            print
+
+    def parse_command_line(self, argv=None):
+        if argv is None:
+            argv = sys.argv[1:]
+        if '-h' in argv or '--h' in argv:
+            self.print_help()
+            sys.exit(1)
+        loader = KeyValueConfigLoader(argv=argv, classes=self.classes)
+        config = loader.load_config()
+        self.config = config
+
+
+def main():
+    app = MyApp()
+    app.parse_command_line()
+    print "app.config:"
+    print app.config
+
+
+if __name__ == "__main__":
+    main()