Repository: ipython/traitlets
Pull request #426 (2017-07-05 22:32:16)

Explain the casing/substring matching of Enums on help-msgs and errors
From the help-messages, config-file headers and invalid-value error-messages on `FuzzyEnum` it's impossible to know whether it is case-sensitive and matches substrings or prefixes.  
That was problematic also because `allow_none`  information was simply omitted.

## Changes:
- Help and Config messages reuse `Enum.info()` instead of `Enum.values` (as `Trait.error(0` does).
- Enhanced `CaselessStrEnum.info()`.
- Changes possibly(?) relevant also for #402.

## Sample output:

- Before:

  ```
  refact(help): reuse `Enum.info()` 

  --TstampSender.send_transfer_encoding=<FuzzyEnum>
      Set the Content-Transfer-Encoding MIME Header and encodes appropriately
      outgoing mails.
      Choices: ['BASE64', 'QUOPRI', '7OR8BIT', 'base64', 'quopri', '7or8bit', 'noenc']
      Default: None
  ```

- AFTER:
  
  ```
  --TstampSender.send_transfer_encoding=<FuzzyEnum>
      Set the Content-Transfer-Encoding MIME Header and encodes appropriately
      outgoing mails.
      Choices: any case-sensitive prefix of ['BASE64', 'QUOPRI', '7OR8BIT', 'base64', 'quopri', '7or8bit', 'noenc'] or None
      Default: None
  ```

ankostis: I think also `class_config_rst_doc()` does not report if `none_allowed`.

Patch:
diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py
index ab3b6d4..83063c6 100644
--- a/traitlets/config/configurable.py
+++ b/traitlets/config/configurable.py
@@ -79,7 +79,7 @@ class Configurable(HasTraits):
 
         # load kwarg traits, other than config
         super(Configurable, self).__init__(**kwargs)
-        
+
         # record traits set by config
         config_override_names = set()
         def notice_config_override(change):
@@ -272,7 +272,7 @@ class Configurable(HasTraits):
 
         if 'Enum' in trait.__class__.__name__:
             # include Enum choices
-            lines.append(indent('Choices: %r' % (trait.values,)))
+            lines.append(indent('Choices: %s' % trait.info()))
 
         if inst is not None:
             lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
@@ -322,7 +322,7 @@ class Configurable(HasTraits):
     @classmethod
     def class_config_section(cls, classes=None):
         """Get the config section for this class.
-        
+
         Parameters
         ----------
         classes: list, optional
@@ -368,7 +368,7 @@ class Configurable(HasTraits):
                     lines.append(c(trait.help))
                 if 'Enum' in type(trait).__name__:
                     # include Enum choices
-                    lines.append('#  Choices: %r' % (trait.values,))
+                    lines.append('#  Choices: %s' % trait.info())
                 lines.append('#  Default: %s' % default_repr)
             else:
                 # Trait appears multiple times and isn't defined here.
diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py
index 253b9aa..85d93ae 100644
--- a/traitlets/traitlets.py
+++ b/traitlets/traitlets.py
@@ -2214,6 +2214,7 @@ class Enum(TraitType):
             return result + ' or None'
         return result
 
+
 class CaselessStrEnum(Enum):
     """An enum of strings where the case should be ignored."""
 
@@ -2232,6 +2233,13 @@ class CaselessStrEnum(Enum):
                 return v
         self.error(obj, value)
 
+    def info(self):
+        """ Returns a description of the trait."""
+        result = 'any of %s (case-insensitive)' % (self.values, )
+        if self.allow_none:
+            return result + ' or None'
+        return result
+
 
 class FuzzyEnum(Enum):
     """An case-ignoring enum matching choices by unique prefixes/substrings."""
@@ -2268,6 +2276,15 @@ class FuzzyEnum(Enum):
 
         self.error(obj, value)
 
+    def info(self):
+        """ Returns a description of the trait."""
+        case = 'sensitive' if self.case_sensitive else 'insensitive'
+        substr = 'substring' if self.substring_matching else 'prefix'
+        result = 'any case-%s %s of %s' % (case, substr, self.values)
+        if self.allow_none:
+            return result + ' or None'
+        return result
+
 
 class Container(Instance):
     """An instance of a container (list, set, etc.)


Test patch:
diff --git a/traitlets/config/tests/test_configurable.py b/traitlets/config/tests/test_configurable.py
index 4baa106..478e727 100644
--- a/traitlets/config/tests/test_configurable.py
+++ b/traitlets/config/tests/test_configurable.py
@@ -18,8 +18,8 @@ from traitlets.config.configurable import (
 )
 
 from traitlets.traitlets import (
-    Integer, Float, Unicode, List, Dict, Set, Enum,
-    _deprecations_shown, validate,
+    Integer, Float, Unicode, List, Dict, Set, Enum, FuzzyEnum,
+    CaselessStrEnum, _deprecations_shown, validate,
 )
 
 from traitlets.config.loader import Config
@@ -192,38 +192,116 @@ class TestConfigurable(TestCase):
                            help="Many choices.").tag(config=True)
 
         help_str = "Many choices."
-        enum_choices_str = "Choices: ['Choice1', 'choice2']"
+        enum_choices_str = "Choices: any of ['Choice1', 'choice2']"
+        or_none_str = "or None"
 
-        self.assertIn(help_str, MyConf.class_get_help())
-        self.assertIn(enum_choices_str, MyConf.class_get_help())
+        cls_help = MyConf.class_get_help()
 
-        self.assertIn(help_str, MyConf.class_config_section())
-        self.assertIn(enum_choices_str, MyConf.class_config_section())
+        self.assertIn(help_str, cls_help)
+        self.assertIn(enum_choices_str, cls_help)
+        self.assertNotIn(or_none_str, cls_help)
+
+        cls_cfg = MyConf.class_config_section()
+
+        self.assertIn(help_str, cls_cfg)
+        self.assertIn(enum_choices_str, cls_cfg)
+        self.assertNotIn(or_none_str, cls_help)
         ## Check order of Help-msg <--> Choices sections
-        self.assertGreater(MyConf.class_config_section().index(enum_choices_str),
-                           MyConf.class_config_section().index(help_str))
+        self.assertGreater(cls_cfg.index(enum_choices_str),
+                           cls_cfg.index(help_str))
 
         class MyConf2(Configurable):
             an_enum = Enum('Choice1 choice2'.split(),
+                           allow_none=True,
                            default_value='choice2',
                            help="Many choices.").tag(config=True)
 
         defaults_str = "Default: 'choice2'"
 
-        self.assertIn(help_str, MyConf2.class_get_help())
-        self.assertIn(enum_choices_str, MyConf2.class_get_help())
-        self.assertIn(defaults_str, MyConf2.class_get_help())
+        cls2_msg = MyConf2.class_get_help()
+
+        self.assertIn(help_str, cls2_msg)
+        self.assertIn(enum_choices_str, cls2_msg)
+        self.assertIn(or_none_str, cls2_msg)
+        self.assertIn(defaults_str, cls2_msg)
+        ## Check order of Default <--> Choices sections
+        self.assertGreater(cls2_msg.index(defaults_str),
+                           cls2_msg.index(enum_choices_str))
+
+        cls2_cfg = MyConf2.class_config_section()
+
+        self.assertIn(help_str, cls2_cfg)
+        self.assertIn(enum_choices_str, cls2_cfg)
+        self.assertIn(or_none_str, cls2_cfg)
+        self.assertIn(defaults_str, cls2_cfg)
+        ## Check order of Default <--> Choices sections
+        self.assertGreater(cls2_cfg.index(defaults_str),
+                           cls2_cfg.index(enum_choices_str))
+
+    @mark.skipif(sys.version_info < (3, ),
+                 reason="Unicodes printed with `u` prefix in PY2!'")
+    def test_generated_config_strenum_comments(self):
+        help_str = "Many choices."
+        defaults_str = "Default: 'choice2'"
+        or_none_str = "or None"
+
+        class MyConf3(Configurable):
+            an_enum = CaselessStrEnum('Choice1 choice2'.split(),
+                                      allow_none=True,
+                                      default_value='choice2',
+                                      help="Many choices.").tag(config=True)
+
+        enum_choices_str = ("Choices: any of ['Choice1', 'choice2'] "
+                            "(case-insensitive)")
+
+        cls3_msg = MyConf3.class_get_help()
+
+        self.assertIn(help_str, cls3_msg)
+        self.assertIn(enum_choices_str, cls3_msg)
+        self.assertIn(or_none_str, cls3_msg)
+        self.assertIn(defaults_str, cls3_msg)
+        ## Check order of Default <--> Choices sections
+        self.assertGreater(cls3_msg.index(defaults_str),
+                           cls3_msg.index(enum_choices_str))
+
+        cls3_cfg = MyConf3.class_config_section()
+
+        self.assertIn(help_str, cls3_cfg)
+        self.assertIn(enum_choices_str, cls3_cfg)
+        self.assertIn(or_none_str, cls3_cfg)
+        self.assertIn(defaults_str, cls3_cfg)
         ## Check order of Default <--> Choices sections
-        self.assertGreater(MyConf2.class_get_help().index(defaults_str),
-                           MyConf2.class_get_help().index(enum_choices_str))
+        self.assertGreater(cls3_cfg.index(defaults_str),
+                           cls3_cfg.index(enum_choices_str))
 
-        self.assertIn(help_str, MyConf2.class_config_section())
-        self.assertIn(enum_choices_str, MyConf2.class_config_section())
-        self.assertIn(defaults_str, MyConf2.class_config_section())
+        class MyConf4(Configurable):
+            an_enum = FuzzyEnum('Choice1 choice2'.split(),
+                                allow_none=True,
+                                default_value='choice2',
+                                help="Many choices.").tag(config=True)
+
+        enum_choices_str = ("Choices: any case-insensitive prefix "
+                            "of ['Choice1', 'choice2']")
+
+        cls4_msg = MyConf4.class_get_help()
+
+        self.assertIn(help_str, cls4_msg)
+        self.assertIn(enum_choices_str, cls4_msg)
+        self.assertIn(or_none_str, cls4_msg)
+        self.assertIn(defaults_str, cls4_msg)
         ## Check order of Default <--> Choices sections
-        self.assertGreater(MyConf2.class_config_section().index(defaults_str),
-                           MyConf2.class_config_section().index(enum_choices_str))
+        self.assertGreater(cls4_msg.index(defaults_str),
+                           cls4_msg.index(enum_choices_str))
 
+        cls4_cfg = MyConf4.class_config_section()
+
+        self.assertIn(help_str, cls4_cfg)
+        self.assertIn(enum_choices_str, cls4_cfg)
+        self.assertIn(or_none_str, cls4_cfg)
+        self.assertIn(defaults_str, cls4_cfg)
+        ## Check order of Default <--> Choices sections
+        self.assertGreater(cls4_cfg.index(defaults_str),
+                           cls4_cfg.index(enum_choices_str))
 
 
 
@@ -499,7 +577,7 @@ class TestConfigContainers(TestCase):
         d2 = DefaultConfigurable()
         self.assertIs(d2.config, single.config)
         self.assertEqual(d2.a, 5)
-    
+
     def test_kwarg_config_priority(self):
         # a, c set in kwargs
         # a, b set in config