Repository: ipython/traitlets
Pull request #361 (2017-01-13 17:40:15)

help: Alias overriding help-text (like flags), options list human-help before automated infos
To keep the API symmetric to *flags*, add the capability to *aliases* to override the printed help-msg.

With this PR, the `Application.aliases` dict may contain *two-tuples* as values like this `(alias: (longname, help-text)}`:

```python
    aliases = Dict({
                    ('fooi', 'i') : 'Foo.i',                          # simple longname
                    ('j', 'fooj') : ('Foo.j', "`j` terse help msg"),  # longname, help-text
    )
```

- Renamed the use of builtin `help` in one occasion;
- changed 2 TCs to test with two-tuples; 
- improved slightly the documentation.

## Rational:
Currently code that prints the help-text for the *aliases* fetches the help-text from the aliased trait, and that is possible since there is a 1-1 correspondence (in contrast to *flags* where it is *1-n*, and thus had to introduce the two-tuple with help-text).
But since this might be long, the end result will be a long help message, scrolling past the end of the screen.
For flags, it is possible (actually forced) to provide *shorter* versions of the help-messages, and use `--help-all` to read all content, but this flexibility is impossible for aliases.

To facilitate comparison, the *before* and *after* help messages are provided for the `--config-paths` alias:

## "Before" Help Messages:
```
$ co2dice --help
prepare/sign/send/receive/validate & archive Type Approval sampling

Options
=======
The options below are convenience aliases to configurable class-options,
as listed in the "Equivalent to" description-line of the aliases.
To see all configurable class-options for some <cmd>, use:
    <cmd> --help-all

-d, --debug
    Log more logging, fail on configuration errors, and print configuration on each cmd startup.
    Equivalent to: [--Application.log_level=0 --Spec.log_level=0 --Cmd.raise_config_file_errors=True]
--config-paths=<list-item-1>...
    Default: []
    Absolute/relative path(s) to read "static" configurable parameters from.
    If false, and no `CO2DICE_CONFIG_PATH` envvar is defined, defaults to:
        ['D:\\Apps\\cygwin64\\home\\anastkn\\.co2dice\\co2dice_config', 'd:\\work\\compas.vinz\\co2mpas\\sampling\\co2dice_config']
    Multiple values may be given, and each value may be a single or multiple paths
    separated by ';'.  All paths collected are considered in descending order
    (1st one overrides the rest).
    For paths resolving to folders, the filename `co2dice_config.[json | py]` is appended;
    otherwise, any file-extensions are ignored, and '.py' and/or '.json' are loaded (in this order).
    Tip:
        Use `config init` sub-command to produce a skeleton of the config-file.

To see all available configurables, use `--help-all`
```

## "After" Help Message:
```
$ co2dice --help
prepare/sign/send/receive/validate & archive Type Approval sampling

Options
=======
The options below are convenience aliases to configurable class-options,
as listed in the "Equivalent to" description-line of the aliases.
To see all configurable class-options for some <cmd>, use:
    <cmd> --help-all

-d, --debug
    Log more logging, fail on configuration errors, and print configuration on each cmd startup.
    Equivalent to: [--Application.log_level=0 --Spec.log_level=0 Cmd.raise_config_file_errors=True]
--config-paths=<list-item-1>...
    Default: []
    Absolute/relative path(s) to read "static" configurable parameters from.

To see all available configurables, use `--help-all`
```


Patch:
diff --git a/docs/source/config.rst b/docs/source/config.rst
index 90d75a9..a97cc39 100644
--- a/docs/source/config.rst
+++ b/docs/source/config.rst
@@ -324,6 +324,10 @@ that are single characters, in which case they can be specified with a single ``
 Flags and aliases are declared by specifying ``flags`` and ``aliases``
 attributes as dictionaries on subclasses of :class:`~traitlets.config.Application`.
 
+A key in both those dictionaries might be a string or tuple of strings.
+One-character strings are converted into "short" options (like ``-v``); longer strings
+are "long" options (like ``--verbose``).
+
 Aliases
 *******
 
@@ -338,6 +342,9 @@ to specify the whole class name:
     # are equivalent to
     $ ipython --BaseIPythonApplication.profile='myprofile'
 
+When specifying ``alias`` dictionary in code, the values might be the strings
+like ``'Class.trait'`` or two-tuples like ``('Class.trait', "Some help message")``.
+
 Flags
 *****
 
@@ -373,7 +380,7 @@ after :command:`git`, and are called with the form :command:`command subcommand
     $ ipython qtconsole --profile myprofile
 
 Subcommands are specified as a dictionary on :class:`~traitlets.config.Application`
-instances, mapping subcommand names to 2-tuples containing:
+instances, mapping subcommand names to two-tuples containing:
 
 1. The application class for the subcommand, or a string which can be imported
    to give this.
diff --git a/traitlets/config/application.py b/traitlets/config/application.py
index 75aaeea..c09c11a 100644
--- a/traitlets/config/application.py
+++ b/traitlets/config/application.py
@@ -227,7 +227,9 @@ class Application(SingletonConfigurable):
         log.addHandler(_log_handler)
         return log
 
-    # the alias map for configurables
+    #: the alias map for configurables
+    #: Keys might strings or tuples for additional options; single-letter alias accessed like `-v`.
+    #: Values might be like "Class.trait" strings of two-tuples: (Class.trait, help-text).
     aliases = Dict({'log-level' : 'Application.log_level'})
 
     # flags for loading Configurables or store_const style flags
@@ -317,11 +319,15 @@ class Application(SingletonConfigurable):
 
         for alias, longname in self.aliases.items():
             try:
-                classname, traitname = longname.split('.',1)
+                if isinstance(longname, tuple):
+                    longname, fhelp = longname
+                else:
+                    fhelp = None
+                classname, traitname = longname.split('.', 1)
                 cls = classdict[classname]
 
                 trait = cls.class_traits(config=True)[traitname]
-                fhelp = cls.class_get_trait_help(trait).splitlines()
+                fhelp = cls.class_get_trait_help(trait, helptext=fhelp).splitlines()
 
                 if not isinstance(alias, tuple):
                     alias = (alias, )
@@ -330,7 +336,7 @@ class Application(SingletonConfigurable):
                                   for m in alias)
 
                 # reformat first line
-                fhelp[0] = fhelp[0].replace('--'+longname, alias)
+                fhelp[0] = fhelp[0].replace('--' + longname, alias)
                 lines.extend(fhelp)
                 lines.append(indent("Equivalent to: [--%s]" % longname))
             except Exception as ex:
@@ -475,11 +481,12 @@ class Application(SingletonConfigurable):
         self.subapp.initialize(argv)
 
     def flatten_flags(self):
-        """flatten flags and aliases, so cl-args override as expected.
+        """Flatten flags and aliases for loaders, so cl-args override as expected.
 
         This prevents issues such as an alias pointing to InteractiveShell,
         but a config file setting the same trait in TerminalInteraciveShell
         getting inappropriate priority over the command-line arg.
+        Also, loaders expect ``(key: longname)`` and not ````key: (longname, help)`` items.
 
         Only aliases with exactly one descendent in the class list
         will be promoted.
@@ -497,8 +504,10 @@ class Application(SingletonConfigurable):
         # flatten aliases, which have the form:
         # { 'alias' : 'Class.trait' }
         aliases = {}
-        for alias, cls_trait in self.aliases.items():
-            cls,trait = cls_trait.split('.',1)
+        for alias, longname in self.aliases.items():
+            if isinstance(longname, tuple):
+                longname, _ = longname
+            cls, trait = longname.split('.', 1)
             children = mro_tree[cls]
             if len(children) == 1:
                 # exactly one descendent, promote alias
diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py
index c32c2f7..bb7ebad 100644
--- a/traitlets/config/configurable.py
+++ b/traitlets/config/configurable.py
@@ -227,11 +227,14 @@ class Configurable(HasTraits):
         return '\n'.join(final_help)
 
     @classmethod
-    def class_get_trait_help(cls, trait, inst=None):
-        """Get the help string for a single trait.
-
-        If `inst` is given, it's current trait values will be used in place of
-        the class default.
+    def class_get_trait_help(cls, trait, inst=None, helptext=None):
+        """Get the helptext string for a single trait.
+
+        :param inst:
+            If given, it's current trait values will be used in place of
+            the class default.
+        :param helptext:
+            If not given, uses the `help` attribute of the current trait.
         """
         assert inst is None or isinstance(inst, cls)
         lines = []
@@ -250,6 +253,13 @@ class Configurable(HasTraits):
             header = '%s=<%s>' % (header, trait.__class__.__name__)
         #header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
         lines.append(header)
+
+        if helptext is None:
+            helptext = trait.help
+        if helptext != '':
+            helptext = '\n'.join(wrap_paragraphs(helptext, 76))
+            lines.append(indent(helptext, 4))
+
         if inst is not None:
             lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
         else:
@@ -265,10 +275,6 @@ class Configurable(HasTraits):
             # include Enum choices
             lines.append(indent('Choices: %r' % (trait.values,)))
 
-        help = trait.help
-        if help != '':
-            help = '\n'.join(wrap_paragraphs(help, 76))
-            lines.append(indent(help, 4))
         return '\n'.join(lines)
 
     @classmethod


Test patch:
diff --git a/traitlets/config/tests/test_application.py b/traitlets/config/tests/test_application.py
index f9efe5c..b96e529 100644
--- a/traitlets/config/tests/test_application.py
+++ b/traitlets/config/tests/test_application.py
@@ -66,7 +66,7 @@ class MyApp(Application):
 
     aliases = Dict({
                     ('fooi', 'i') : 'Foo.i',
-                    ('j', 'fooj') : 'Foo.j',
+                    ('j', 'fooj') : ('Foo.j', "`j` terse help msg"),
                     'name' : 'Foo.name',
                     'la': 'Foo.la',
                     'tb': 'Bar.tb',
@@ -194,7 +194,7 @@ class TestApplication(TestCase):
         class TestApp(Application):
             value = Unicode().tag(config=True)
             config_file_loaded = Bool().tag(config=True)
-            aliases = {'v': 'TestApp.value'}
+            aliases = {'v': ('TestApp.value', 'some help')}
         app = TestApp()
         with TemporaryDirectory() as td:
             config_file = pjoin(td, name)
diff --git a/traitlets/config/tests/test_configurable.py b/traitlets/config/tests/test_configurable.py
index 97ece8e..8a95ca3 100644
--- a/traitlets/config/tests/test_configurable.py
+++ b/traitlets/config/tests/test_configurable.py
@@ -35,20 +35,20 @@ class MyConfigurable(Configurable):
 mc_help=u"""MyConfigurable(Configurable) options
 ------------------------------------
 --MyConfigurable.a=<Integer>
-    Default: 1
     The integer a.
+    Default: 1
 --MyConfigurable.b=<Float>
-    Default: 1.0
-    The integer b."""
+    The integer b.
+    Default: 1.0"""
 
 mc_help_inst=u"""MyConfigurable(Configurable) options
 ------------------------------------
 --MyConfigurable.a=<Integer>
-    Current: 5
     The integer a.
+    Current: 5
 --MyConfigurable.b=<Float>
-    Current: 4.0
-    The integer b."""
+    The integer b.
+    Current: 4.0"""
 
 # On Python 3, the Integer trait is a synonym for Int
 if PY3:
@@ -71,8 +71,8 @@ class Bar(Foo):
 foo_help=u"""Foo(Configurable) options
 -------------------------
 --Foo.a=<Int>
-    Default: 0
     The integer a.
+    Default: 0
 --Foo.b=<Unicode>
     Default: 'nope'
 --Foo.fdict=<key-1>=<value-1>...
@@ -83,15 +83,15 @@ foo_help=u"""Foo(Configurable) options
 bar_help=u"""Bar(Foo) options
 ----------------
 --Bar.a=<Int>
-    Default: 0
     The integer a.
+    Default: 0
 --Bar.bdict <key-1>=<value-1>...
     Default: {}
 --Bar.bset <set-item-1>...
     Default: set()
 --Bar.c=<Float>
-    Default: 0.0
     The string c.
+    Default: 0.0
 --Bar.fdict=<key-1>=<value-1>...
     Default: {}
 --Bar.flist=<list-item-1>...