diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py index ab3b6d40..83063c61 100644 --- a/traitlets/config/configurable.py +++ b/traitlets/config/configurable.py @@ -79,7 +79,7 @@ def __init__(self, config=None): # 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 @@ def class_get_trait_help(cls, trait, inst=None, helptext=None): 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 @@ def _defining_class(cls, trait, classes): @classmethod def class_config_section(cls, classes=None): """Get the config section for this class. - + Parameters ---------- classes: list, optional @@ -368,7 +368,7 @@ def c(s): 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/config/tests/test_configurable.py b/traitlets/config/tests/test_configurable.py index 4baa1069..478e7279 100644 --- a/traitlets/config/tests/test_configurable.py +++ b/traitlets/config/tests/test_configurable.py @@ -18,8 +18,8 @@ ) 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 MyConf(Configurable): 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 @@ def _config_default(self): 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 diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index 253b9aa9..85d93ae8 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -2214,6 +2214,7 @@ def info(self): return result + ' or None' return result + class CaselessStrEnum(Enum): """An enum of strings where the case should be ignored.""" @@ -2232,6 +2233,13 @@ def validate(self, obj, value): 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 @@ def validate(self, obj, value): 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.)