Repository: ipython/traitlets
Pull request #438 (2017-08-12 02:31:06)

fix(help): finish #381 for RsT to print enum-choices
#381 inserted  a new line `Choices: x, y, z` into the help and config messages for all *Enum* traits, immediately above `Default: ...` line.
As had been mentioned in that PR, that feature was never added to the *RsT* documentation produced by `Configurable.class_config_rst_doc()` classs method.
This PR fixes that neglect.

+ Had to add an optional `as_rst=false` kwd to all`XxxEnum.info()` methods.
+ Added TCs for `info()`.

## RsT samples for `info()` method:
- `Enum` & `UseEnum` traits:

      any of ``'red'``|``'Green'``|``'BLUE'``|``'YeLLoW'`` or `None`

- `CaselessStrEnum`:

      any of ``'red'``|``'Green'``|``'BLUE'``|``'YeLLoW'`` (case-insensitive) or `None`

- `FuzzyEnum`:

      any case-insensitive prefix of ``'red'``|``'Green'``|``'BLUE'``|``'YeLLoW'`` or `None`


Patch:
diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py
index 83063c6..05495d3 100644
--- a/traitlets/config/configurable.py
+++ b/traitlets/config/configurable.py
@@ -397,7 +397,7 @@ class Configurable(HasTraits):
             # Choices or type
             if 'Enum' in ttype:
                 # include Enum choices
-                termline += ' : ' + '|'.join(repr(x) for x in trait.values)
+                termline += ' : ' + trait.info_rst()
             else:
                 termline += ' : ' + ttype
             lines.append(termline)
diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py
index fa883de..bd04974 100644
--- a/traitlets/traitlets.py
+++ b/traitlets/traitlets.py
@@ -1190,7 +1190,7 @@ class HasTraits(six.with_metaclass(MetaHasTraits, HasDescriptors)):
     def notify_change(self, change):
         """Notify observers of a change event"""
         return self._notify_observers(change)
-    
+
     def _notify_observers(self, event):
         """Notify observers of any event"""
         if not isinstance(event, Bunch):
@@ -2263,12 +2263,27 @@ class Enum(TraitType):
                 return value
         self.error(obj, value)
 
-    def info(self):
+    def _choices_str(self, as_rst=False):
+        """ Returns a description of the trait choices (not none)."""
+        choices = self.values
+        if as_rst:
+            choices = '|'.join('``%r``' % x for x in choices)
+        else:
+            choices = repr(list(choices))
+        return choices
+
+    def _info(self, as_rst=False):
         """ Returns a description of the trait."""
-        result = 'any of ' + repr(self.values)
-        if self.allow_none:
-            return result + ' or None'
-        return result
+        none = (' or %s' % ('`None`' if as_rst else 'None')
+                if self.allow_none else
+                '')
+        return 'any of %s%s' % (self._choices_str(as_rst), none)
+
+    def info(self):
+        return self._info(as_rst=False)
+
+    def info_rst(self):
+        return self._info(as_rst=True)
 
 
 class CaselessStrEnum(Enum):
@@ -2289,12 +2304,18 @@ class CaselessStrEnum(Enum):
                 return v
         self.error(obj, value)
 
-    def info(self):
+    def _info(self, as_rst=False):
         """ Returns a description of the trait."""
-        result = 'any of %s (case-insensitive)' % (self.values, )
-        if self.allow_none:
-            return result + ' or None'
-        return result
+        none = (' or %s' % ('`None`' if as_rst else 'None')
+                if self.allow_none else
+                '')
+        return 'any of %s (case-insensitive)%s' % (self._choices_str(as_rst), none)
+
+    def info(self):
+        return self._info(as_rst=False)
+
+    def info_rst(self):
+        return self._info(as_rst=True)
 
 
 class FuzzyEnum(Enum):
@@ -2332,14 +2353,22 @@ class FuzzyEnum(Enum):
 
         self.error(obj, value)
 
-    def info(self):
+    def _info(self, as_rst=False):
         """ Returns a description of the trait."""
+        none = (' or %s' % ('`None`' if as_rst else 'None')
+                if self.allow_none else
+                '')
         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
+        return 'any case-%s %s of %s%s' % (case, substr,
+                                           self._choices_str(as_rst),
+                                           none)
+
+    def info(self):
+        return self._info(as_rst=False)
+
+    def info_rst(self):
+        return self._info(as_rst=True)
 
 
 class Container(Instance):
@@ -2921,12 +2950,27 @@ class UseEnum(TraitType):
                 return self.default_value
         self.error(obj, value)
 
+    def _choices_str(self, as_rst=False):
+        """ Returns a description of the trait choices (not none)."""
+        choices = self.enum_class.__members__.keys()
+        if as_rst:
+            return '|'.join('``%r``' % x for x in choices)
+        else:
+            return repr(list(choices))  # Listify because py3.4- prints odict-class
+
+    def _info(self, as_rst=False):
+        """ Returns a description of the trait."""
+        none = (' or %s' % ('`None`' if as_rst else 'None')
+                if self.allow_none else
+                '')
+        return 'any of %s%s' % (self._choices_str(as_rst), none)
+
     def info(self):
-        """Returns a description of this Enum trait (in case of errors)."""
-        result = "Any of: %s" % ", ".join(self.enum_class.__members__.keys())
-        if self.allow_none:
-            return result + " or None"
-        return result
+        return self._info(as_rst=False)
+
+    def info_rst(self):
+        return self._info(as_rst=True)
+
 
 class Callable(TraitType):
     """A trait which is callable.


Test patch:
diff --git a/traitlets/config/tests/test_configurable.py b/traitlets/config/tests/test_configurable.py
index 478e727..3ba3c9e 100644
--- a/traitlets/config/tests/test_configurable.py
+++ b/traitlets/config/tests/test_configurable.py
@@ -193,6 +193,7 @@ class TestConfigurable(TestCase):
 
         help_str = "Many choices."
         enum_choices_str = "Choices: any of ['Choice1', 'choice2']"
+        rst_choices_str = "MyConf.an_enum : any of ``'Choice1'``|``'choice2'``"
         or_none_str = "or None"
 
         cls_help = MyConf.class_get_help()
@@ -210,6 +211,12 @@ class TestConfigurable(TestCase):
         self.assertGreater(cls_cfg.index(enum_choices_str),
                            cls_cfg.index(help_str))
 
+        rst_help = MyConf.class_config_rst_doc()
+
+        self.assertIn(help_str, rst_help)
+        self.assertIn(rst_choices_str, rst_help)
+        self.assertNotIn(or_none_str, rst_help)
+
         class MyConf2(Configurable):
             an_enum = Enum('Choice1 choice2'.split(),
                            allow_none=True,
diff --git a/traitlets/tests/test_traitlets_enum.py b/traitlets/tests/test_traitlets_enum.py
index a015f7c..8caf9d2 100644
--- a/traitlets/tests/test_traitlets_enum.py
+++ b/traitlets/tests/test_traitlets_enum.py
@@ -7,7 +7,7 @@ Test the trait-type ``UseEnum``.
 import unittest
 import enum
 from ipython_genutils.py3compat import string_types
-from traitlets import HasTraits, TraitError, UseEnum, FuzzyEnum
+from traitlets import HasTraits, TraitError, Enum, UseEnum, CaselessStrEnum, FuzzyEnum
 
 
 # -----------------------------------------------------------------------------
@@ -25,6 +25,16 @@ class OtherColor(enum.Enum):
     green = 1
 
 
+class CSColor(enum.Enum):
+    red = 1
+    Green = 2
+    BLUE = 3
+    YeLLoW = 4
+
+
+color_choices = 'red Green  BLUE YeLLoW'.split()
+
+
 # -----------------------------------------------------------------------------
 # TESTSUITE:
 # -----------------------------------------------------------------------------
@@ -180,11 +190,46 @@ class TestUseEnum(unittest.TestCase):
         with self.assertRaises(TraitError):
             example.color = "BAD_VALUE"
 
+    def test_info(self):
+        import sys
+
+        choices = color_choices
+        class Example(HasTraits):
+            enum1 = Enum(choices, allow_none=False)
+            enum2 = CaselessStrEnum(choices, allow_none=False)
+            enum3 = FuzzyEnum(choices, allow_none=False)
+            enum4 = UseEnum(CSColor, allow_none=False)
+
+        for i in range(1,5):
+            attr = 'enum%s' % i
+            enum = getattr(Example, attr)
+
+            enum.allow_none = True
+
+            info = enum.info()
+            self.assertEqual(len(info.split(', ')), len(choices), info.split(', '))
+            self.assertIn('or None', info)
+
+            info = enum.info_rst()
+            self.assertEqual(len(info.split('|')), len(choices), info.split('|'))
+            self.assertIn('or `None`', info)
+            ## Check no single `\` exists.
+            if sys.version_info >= (3, ):
+                self.assertNotRegex(info, r'\b\\\b')
+
+            enum.allow_none = False
+
+            info = enum.info()
+            self.assertEqual(len(info.split(', ')), len(choices), info.split(', '))
+            self.assertNotIn('None', info)
+
+            info = enum.info_rst()
+            self.assertEqual(len(info.split('|')), len(choices), info.split('|'))
+            self.assertNotIn('None', info)
+            ## Check no single `\` exists.
+            if sys.version_info >= (3, ):
+                self.assertNotRegex(info, r'\b\\\b')
 
-# -----------------------------------------------------------------------------
-# TEST SUPPORT:
-# -----------------------------------------------------------------------------
-color_choices = 'red Green  BLUE YeLLoW'.split()
 
 
 # -----------------------------------------------------------------------------