Repository: ipython/traitlets (python)

Issue:
Enhance `--help-all` to print all relevant  configurables, along with their relations
Now that an elaborate traversal of **"Configurable-inheritance"**  got merged (#289) for when generating the sample config-file (hopefully everything works all right),
is there a reason why the same logic is not used when printing the `--help-all`  message?
(i.e. to reduce the clutter in the terminal?)

Specifically in [traitlets/config/application.py#L384](https://github.com/ipython/traitlets/blob/master/traitlets/config/application.py#L384) where the classes to examine gets decided, there is this line:

    help_classes = self.classes

I would expect to find:

    help_classes =  = self._classes_inc_parents(self)  

but I've tested with even with the new `self._classes_in_config_sample()` method and it works fine.

A PR for this would be trivial, but now that `parent` is appropriately set on sub-commands (#292),
~~we can~~ *it may be possible to*  traverse the **"commands-chain"**, to provide also hints about them on the help & sample-config-file sections, like that:  

    [root-cmd  previous-sub-cmd]  *sub-cmd(parent1, parent2)

[*] It is the `sub-cmd` that is documented above.


Pull request description:
app: support flags/aliases given as (--long, -short) tuple
+ Specify both short/long forms of flags & aliases at once.
+ Explain explicitely equivalence of flags/aliases.
+ Handle and log exceptions while printing flag/alias help-msgs - stacktrace was too convoluted otherwise.
+ FIX #296: print all relevant (sub)classes in help-msg.

An example of the new flags/aliases help message:

```
-f, --force
    Force various sub-commands perform their duties without complaints.
    Equivalent to: [Spec.force=True]
-v, --verbose
    Make various sub-commands increase their verbosity (not to be confused with --debug):
    Equivalent to: [Spec.verbose=True]
-d, --debug
    Log more logging, fail on configuration errors, and print configuration on each cmd startup.
    Equivalent to: [Cmd.raise_config_file_errors=True Cmd.print_config=True Spec.log_level=0 Application.log_level=0]
--log-level=<Enum>
    Default: 30
    Choices: (0, 10, 20, 30, 40, 50, 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL')
    Set the log level by value or name.
    Equivalent to: [Application.log_level]
```

Patch:
diff --git a/traitlets/config/application.py b/traitlets/config/application.py
index 0d886f0..b79062b 100644
--- a/traitlets/config/application.py
+++ b/traitlets/config/application.py
@@ -312,16 +312,28 @@ class Application(SingletonConfigurable):
                 classdict[c.__name__] = c
 
         for alias, longname in self.aliases.items():
-            classname, traitname = longname.split('.',1)
-            cls = classdict[classname]
-
-            trait = cls.class_traits(config=True)[traitname]
-            help = cls.class_get_trait_help(trait).splitlines()
-            # reformat first line
-            help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
-            if len(alias) == 1:
-                help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
-            lines.extend(help)
+            try:
+                classname, traitname = longname.split('.',1)
+                cls = classdict[classname]
+
+                trait = cls.class_traits(config=True)[traitname]
+                fhelp = cls.class_get_trait_help(trait).splitlines()
+
+                if not isinstance(alias, tuple):
+                    alias = (alias, )
+                alias = sorted(alias, key=len)
+                alias = ', '.join(('--%s' if len(m) > 1 else '-%s') % m
+                                  for m in alias)
+
+                # reformat first line
+                fhelp[0] = fhelp[0].replace('--'+longname, alias)
+                lines.extend(fhelp)
+                lines.append(indent("Equivalent to: [--%s]" % longname))
+            except Exception as ex:
+                self.log.error('Failed collecting help-message for alias %r, due to: %s',
+                               alias, ex)
+                raise
+
         # lines.append('')
         print(os.linesep.join(lines))
 
@@ -331,10 +343,25 @@ class Application(SingletonConfigurable):
             return
 
         lines = []
-        for m, (cfg,help) in self.flags.items():
-            prefix = '--' if len(m) > 1 else '-'
-            lines.append(prefix+m)
-            lines.append(indent(dedent(help.strip())))
+        for flags, (cfg, fhelp) in self.flags.items():
+            try:
+                if not isinstance(flags, tuple):
+                    flags = (flags, )
+                flags = sorted(flags, key=len)
+                flags = ', '.join(('--%s' if len(m) > 1 else '-%s') % m
+                              for m in flags)
+                lines.append(flags)
+                lines.append(indent(dedent(fhelp.strip())))
+                cfg_list = ' '.join('--%s.%s=%s' %(clname, prop, val)
+                            for clname, props_dict
+                            in cfg.items() for prop, val in props_dict.items())
+                cfg_txt = "Equivalent to: [%s]" % cfg_list
+                lines.append(indent(dedent(cfg_txt)))
+            except Exception as ex:
+                self.log.error('Failed collecting help-message for flag %r, due to: %s',
+                               flags, ex)
+                raise
+
         # lines.append('')
         print(os.linesep.join(lines))
 
@@ -381,7 +408,7 @@ class Application(SingletonConfigurable):
         self.print_options()
 
         if classes:
-            help_classes = self.classes
+            help_classes = self._classes_in_config_sample()
             if help_classes:
                 print("Class parameters")
                 print("----------------")
@@ -475,7 +502,10 @@ class Application(SingletonConfigurable):
             if len(children) == 1:
                 # exactly one descendent, promote alias
                 cls = children[0]
-            aliases[alias] = '.'.join([cls,trait])
+            if not isinstance(aliases, tuple):
+                alias = (alias, )
+            for al in alias:
+                aliases[al] = '.'.join([cls,trait])
 
         # flatten flags, which are of the form:
         # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
@@ -488,7 +518,10 @@ class Application(SingletonConfigurable):
                 if len(children) == 1:
                     cls = children[0]
                 newflag[cls] = subdict
-            flags[key] = (newflag, help)
+            if not isinstance(key, tuple):
+                key = (key, )
+            for k in key:
+                flags[k] = (newflag, help)
         return flags, aliases
 
     @catch_config_error
@@ -581,7 +614,7 @@ class Application(SingletonConfigurable):
                     yield config
                     loaded.append(config)
                     filenames.append(loader.full_filename)
-            
+
 
 
     @catch_config_error
diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py
index 1174fcf..bbc1060 100644
--- a/traitlets/config/configurable.py
+++ b/traitlets/config/configurable.py
@@ -210,7 +210,8 @@ class Configurable(HasTraits):
         """
         assert inst is None or isinstance(inst, cls)
         final_help = []
-        final_help.append(u'%s options' % cls.__name__)
+        base_classes = ','.join(p.__name__ for p in cls.__bases__)
+        final_help.append(u'%s(%s) options' % (cls.__name__, base_classes))
         final_help.append(len(final_help[0])*u'-')
         for k, v in sorted(cls.class_traits(config=True).items()):
             help = cls.class_get_trait_help(v, inst)
diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py
index 803b362..3a75350 100644
--- a/traitlets/config/loader.py
+++ b/traitlets/config/loader.py
@@ -786,25 +786,31 @@ class KVArgParseConfigLoader(ArgParseConfigLoader):
         if flags is None:
             flags = self.flags
         paa = self.parser.add_argument
-        for key,value in aliases.items():
-            if key in flags:
-                # flags
-                nargs = '?'
-            else:
-                nargs = None
-            if len(key) is 1:
-                paa('-'+key, '--'+key, type=text_type, dest=value, nargs=nargs)
-            else:
-                paa('--'+key, type=text_type, dest=value, nargs=nargs)
-        for key, (value, help) in flags.items():
-            if key in self.aliases:
-                #
-                self.alias_flags[self.aliases[key]] = value
-                continue
-            if len(key) is 1:
-                paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
-            else:
-                paa('--'+key, action='append_const', dest='_flags', const=value)
+        for keys,value in aliases.items():
+            if not isinstance(keys, tuple):
+                keys = (keys, )
+            for key in keys:
+                if key in flags:
+                    # flags
+                    nargs = '?'
+                else:
+                    nargs = None
+                if len(key) is 1:
+                    paa('-'+key, '--'+key, type=text_type, dest=value, nargs=nargs)
+                else:
+                    paa('--'+key, type=text_type, dest=value, nargs=nargs)
+        for keys, (value, help) in flags.items():
+            if not isinstance(keys, tuple):
+                keys = (keys, )
+            for key in keys:
+                if key in self.aliases:
+                    #
+                    self.alias_flags[self.aliases[key]] = value
+                    continue
+                if len(key) is 1:
+                    paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
+                else:
+                    paa('--'+key, action='append_const', dest='_flags', const=value)
 
     def _convert_to_config(self):
         """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""


Test patch:
diff --git a/traitlets/config/tests/test_application.py b/traitlets/config/tests/test_application.py
index 5db0243..dcc3c2f 100644
--- a/traitlets/config/tests/test_application.py
+++ b/traitlets/config/tests/test_application.py
@@ -6,9 +6,12 @@ Tests for traitlets.config.application.Application
 # Copyright (c) IPython Development Team.
 # Distributed under the terms of the Modified BSD License.
 
+import contextlib
+import io
 import json
 import logging
 import os
+import sys
 from io import StringIO
 from unittest import TestCase
 
@@ -34,7 +37,6 @@ from traitlets.traitlets import (
     Bool, Unicode, Integer, List, Dict
 )
 
-
 class Foo(Configurable):
 
     i = Integer(0, help="The integer i.").tag(config=True)
@@ -59,18 +61,23 @@ class MyApp(Application):
             help="Should print a warning if `MyApp.warn-typo=...` command is passed")
 
     aliases = Dict({
-                    'i' : 'Foo.i',
-                    'j' : 'Foo.j',
+                    ('fooi', 'i') : 'Foo.i',
+                    ('j', 'fooj') : 'Foo.j',
                     'name' : 'Foo.name',
                     'enabled' : 'Bar.enabled',
                     'log-level' : 'Application.log_level',
                 })
 
-    flags = Dict(dict(enable=({'Bar': {'enabled' : True}}, "Set Bar.enabled to True"),
-                  disable=({'Bar': {'enabled' : False}}, "Set Bar.enabled to False"),
-                  crit=({'Application' : {'log_level' : logging.CRITICAL}},
+    flags = Dict({('enable', 'e'):
+                        ({'Bar': {'enabled' : True}},
+                         "Set Bar.enabled to True"),
+                  ('d', 'disable'):
+                        ({'Bar': {'enabled' : False}},
+                         "Set Bar.enabled to False"),
+                  'crit':
+                        ({'Application' : {'log_level' : logging.CRITICAL}},
                         "set level=CRITICAL"),
-            ))
+            })
 
     def init_foo(self):
         self.foo = Foo(parent=self)
@@ -183,10 +190,35 @@ class TestApplication(TestCase):
         app.parse_command_line(["--disable"])
         app.init_bar()
         self.assertEqual(app.bar.enabled, False)
+
+        app = MyApp()
+        app.parse_command_line(["-d"])
+        app.init_bar()
+        self.assertEqual(app.bar.enabled, False)
+
+        app = MyApp()
         app.parse_command_line(["--enable"])
         app.init_bar()
         self.assertEqual(app.bar.enabled, True)
 
+        app = MyApp()
+        app.parse_command_line(["-e"])
+        app.init_bar()
+        self.assertEqual(app.bar.enabled, True)
+
+    @mark.skipif(sys.version_info < (3, 4),
+                 reason="Missing `contextlib.redirect_stdout` in python < 3.4!")
+    def test_flags_help_msg(self):
+        app = MyApp()
+        stdout = io.StringIO()
+        with contextlib.redirect_stdout(stdout):
+            app.print_flag_help()
+        hmsg = stdout.getvalue()
+        self.assertRegex(hmsg, "(?<!-)-e, --enable\\b")
+        self.assertRegex(hmsg, "(?<!-)-d, --disable\\b")
+        self.assertIn("Equivalent to: [--Bar.enabled=True]", hmsg)
+        self.assertIn("Equivalent to: [--Bar.enabled=False]", hmsg)
+
     def test_aliases(self):
         app = MyApp()
         app.parse_command_line(["--i=5", "--j=10"])
@@ -195,6 +227,34 @@ class TestApplication(TestCase):
         app.init_foo()
         self.assertEqual(app.foo.j, 10)
 
+        app = MyApp()
+        app.parse_command_line(["-i=5", "-j=10"])
+        app.init_foo()
+        self.assertEqual(app.foo.i, 5)
+        app.init_foo()
+        self.assertEqual(app.foo.j, 10)
+
+        app = MyApp()
+        app.parse_command_line(["--fooi=5", "--fooj=10"])
+        app.init_foo()
+        self.assertEqual(app.foo.i, 5)
+        app.init_foo()
+        self.assertEqual(app.foo.j, 10)
+
+    @mark.skipif(sys.version_info < (3, 4),
+                 reason="Missing `contextlib.redirect_stdout` in python < 3.4!")
+    def test_aliases_help_msg(self):
+        app = MyApp()
+        stdout = io.StringIO()
+        with contextlib.redirect_stdout(stdout):
+            app.print_alias_help()
+        hmsg = stdout.getvalue()
+        self.assertRegex(hmsg, "(?<!-)-i, --fooi\\b")
+        self.assertRegex(hmsg, "(?<!-)-j, --fooj\\b")
+        self.assertIn("Equivalent to: [--Foo.i]", hmsg)
+        self.assertIn("Equivalent to: [--Foo.j]", hmsg)
+        self.assertIn("Equivalent to: [--Foo.name]", hmsg)
+
     def test_flag_clobber(self):
         """test that setting flags doesn't clobber existing settings"""
         app = MyApp()
diff --git a/traitlets/config/tests/test_configurable.py b/traitlets/config/tests/test_configurable.py
index cf269c0..fb2e2d5 100644
--- a/traitlets/config/tests/test_configurable.py
+++ b/traitlets/config/tests/test_configurable.py
@@ -31,8 +31,8 @@ class MyConfigurable(Configurable):
     c = Unicode('no config')
 
 
-mc_help=u"""MyConfigurable options
-----------------------
+mc_help=u"""MyConfigurable(Configurable) options
+------------------------------------
 --MyConfigurable.a=<Integer>
     Default: 1
     The integer a.
@@ -40,8 +40,8 @@ mc_help=u"""MyConfigurable options
     Default: 1.0
     The integer b."""
 
-mc_help_inst=u"""MyConfigurable options
-----------------------
+mc_help_inst=u"""MyConfigurable(Configurable) options
+------------------------------------
 --MyConfigurable.a=<Integer>
     Current: 5
     The integer a.
@@ -89,7 +89,7 @@ class TestConfigurable(TestCase):
         self.assertTrue(c3.config is config)
         self.assertTrue(c1.config is c2.config)
         self.assertTrue(c2.config is c3.config)
-        
+
     def test_inheritance(self):
         config = Config()
         config.MyConfigurable.a = 2
@@ -179,7 +179,7 @@ class MyParent2(MyParent):
     pass
 
 class TestParentConfigurable(TestCase):
-    
+
     def test_parent_config(self):
         cfg = Config({
             'MyParent' : {
@@ -275,11 +275,11 @@ class Containers(Configurable):
     lis = List().tag(config=True)
     def _lis_default(self):
         return [-1]
-    
+
     s = Set().tag(config=True)
     def _s_default(self):
         return {'a'}
-    
+
     d = Dict().tag(config=True)
     def _d_default(self):
         return {'a' : 'b'}
@@ -346,22 +346,22 @@ class TestConfigContainers(TestCase):
         c.Containers.d.update({'e' : 'f'})
         obj = Containers(config=c)
         self.assertEqual(obj.d, {'a':'b', 'c':'d', 'e':'f'})
-    
+
     def test_update_twice(self):
         c = Config()
         c.MyConfigurable.a = 5
         m = MyConfigurable(config=c)
         self.assertEqual(m.a, 5)
-        
+
         c2 = Config()
         c2.MyConfigurable.a = 10
         m.update_config(c2)
         self.assertEqual(m.a, 10)
-        
+
         c2.MyConfigurable.a = 15
         m.update_config(c2)
         self.assertEqual(m.a, 15)
-    
+
     def test_update_self(self):
         """update_config with same config object still triggers config_changed"""
         c = Config()
@@ -371,7 +371,7 @@ class TestConfigContainers(TestCase):
         c.MyConfigurable.a = 10
         m.update_config(c)
         self.assertEqual(m.a, 10)
-    
+
     def test_config_default(self):
         class SomeSingleton(SingletonConfigurable):
             pass
@@ -388,9 +388,9 @@ class TestConfigContainers(TestCase):
 
         d1 = DefaultConfigurable()
         self.assertEqual(d1.a, 0)
-        
+
         single = SomeSingleton.instance(config=c)
-        
+
         d2 = DefaultConfigurable()
         self.assertIs(d2.config, single.config)
         self.assertEqual(d2.a, 5)
@@ -415,9 +415,9 @@ class TestConfigContainers(TestCase):
 
         d1 = DefaultConfigurable()
         self.assertEqual(d1.a, 0)
-        
+
         single = SomeSingleton.instance(config=c)
-        
+
         d2 = DefaultConfigurable()
         self.assertIs(d2.config, single.config)
         self.assertEqual(d2.a, 5)
@@ -429,14 +429,14 @@ class TestLogger(TestCase):
             foo = Integer(config=True)
             bar = Integer(config=True)
             baz = Integer(config=True)
-    
+
     @mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs')
     def test_warn_match(self):
         logger = logging.getLogger('test_warn_match')
         cfg = Config({'A': {'bat': 5}})
         with self.assertLogs(logger, logging.WARNING) as captured:
             a = TestLogger.A(config=cfg, log=logger)
-        
+
         output = '\n'.join(captured.output)
         self.assertIn('Did you mean one of: `bar, baz`?', output)
         self.assertIn('Config option `bat` not recognized by `A`.', output)
@@ -444,7 +444,7 @@ class TestLogger(TestCase):
         cfg = Config({'A': {'fool': 5}})
         with self.assertLogs(logger, logging.WARNING) as captured:
             a = TestLogger.A(config=cfg, log=logger)
-        
+
         output = '\n'.join(captured.output)
         self.assertIn('Config option `fool` not recognized by `A`.', output)
         self.assertIn('Did you mean `foo`?', output)