diff --git a/IPython/config/application.py b/IPython/config/application.py index 2dab68b..b035437 100644 --- a/IPython/config/application.py +++ b/IPython/config/application.py @@ -23,13 +23,15 @@ import logging import sys from IPython.config.configurable import SingletonConfigurable -from IPython.utils.traitlets import ( - Unicode, List, Int, Enum -) from IPython.config.loader import ( KeyValueConfigLoader, PyFileConfigLoader ) +from IPython.utils.traitlets import ( + Unicode, List, Int, Enum, Dict +) +from IPython.utils.text import indent + #----------------------------------------------------------------------------- # Application class #----------------------------------------------------------------------------- @@ -55,16 +57,33 @@ class Application(SingletonConfigurable): # The log level for the application log_level = Enum((0,10,20,30,40,50), default_value=logging.WARN, - config=True, shortname="log_level", + config=True, help="Set the log level (0,10,20,30,40,50).") + + # the shortname map for configurables + shortnames = Dict(dict(log_level='Application.log_level')) + + # macros for loading Configurables or store_const style flags + # macros are loaded from this dict by '--key' flags + macros = Dict() + # macro_help dict keys must match macros + macro_help = Dict() + def __init__(self, **kwargs): SingletonConfigurable.__init__(self, **kwargs) # Add my class to self.classes so my attributes appear in command line # options. self.classes.insert(0, self.__class__) + + # check that macro_help has the right keys + # there is probably a better way to do this that doesn't use 2 dicts + keys = set(self.macros.keys()) + badkeys = keys.difference_update(set(self.macro_help.keys())) + if badkeys: + raise KeyError("macro %r has no help in `macro_help`!"%badkeys.pop()) self.init_logging() - + def init_logging(self): """Start logging for this application. @@ -82,9 +101,45 @@ class Application(SingletonConfigurable): def _log_level_changed(self, name, old, new): """Adjust the log level when log_level is set.""" self.log.setLevel(new) - + + def print_shortname_help(self): + """print the shortname part of the help""" + if not self.shortnames: + return + + print "Aliases" + print "-------" + classdict = {} + for c in self.classes: + classdict[c.__name__] = c + for shortname, longname in self.shortnames.iteritems(): + classname, traitname = longname.split('.',1) + cls = classdict[classname] + + trait = cls.class_traits(config=True)[traitname] + help = trait.get_metadata('help') + print shortname, "(%s)"%longname, ':', trait.__class__.__name__ + if help: + print indent(help) + print + + def print_macro_help(self): + """print the the macro part of the help""" + if not self.macros: + return + + print "Flags" + print "-----" + + for m, cfg in self.macros.iteritems(): + print '--'+m + print indent(self.macro_help[m]) + print + def print_help(self): """Print the help for each Configurable class in self.classes.""" + self.print_macro_help() + self.print_shortname_help() for cls in self.classes: cls.class_print_help() print @@ -112,7 +167,7 @@ class Application(SingletonConfigurable): """Parse the command line arguments.""" argv = sys.argv[1:] if argv is None else argv - if '-h' in argv or '--h' in argv: + if '-h' in argv or '--help' in argv: self.print_description() self.print_help() sys.exit(1) @@ -121,7 +176,8 @@ class Application(SingletonConfigurable): self.print_version() sys.exit(1) - loader = KeyValueConfigLoader(argv=argv, classes=self.classes) + loader = KeyValueConfigLoader(argv=argv, shortnames=self.shortnames, + macros=self.macros) config = loader.load_config() self.update_config(config) diff --git a/IPython/config/configurable.py b/IPython/config/configurable.py index a0dae9f..89e6038 100755 --- a/IPython/config/configurable.py +++ b/IPython/config/configurable.py @@ -140,18 +140,6 @@ class Configurable(HasTraits): setattr(self, k, deepcopy(config_value)) @classmethod - def class_get_shortnames(cls): - """Return the shortname to fullname dict for config=True traits.""" - cls_traits = cls.class_traits(config=True) - shortnames = {} - for k, v in cls_traits.items(): - shortname = v.get_metadata('shortname') - if shortname is not None: - longname = cls.__name__ + '.' + k - shortnames[shortname] = longname - return shortnames - - @classmethod def class_get_help(cls): """Get the help string for this class in ReST format.""" cls_traits = cls.class_traits(config=True) @@ -160,10 +148,7 @@ class Configurable(HasTraits): final_help.append(len(final_help[0])*u'-') for k, v in cls_traits.items(): help = v.get_metadata('help') - shortname = v.get_metadata('shortname') header = "%s.%s : %s" % (cls.__name__, k, v.__class__.__name__) - if shortname is not None: - header += " (shortname=" + shortname + ")" final_help.append(header) if help is not None: final_help.append(indent(help)) diff --git a/IPython/config/loader.py b/IPython/config/loader.py index 01fa26e..db10c6c 100644 --- a/IPython/config/loader.py +++ b/IPython/config/loader.py @@ -18,6 +18,7 @@ Authors #----------------------------------------------------------------------------- import __builtin__ +import re import sys from IPython.external import argparse @@ -303,6 +304,8 @@ class CommandLineConfigLoader(ConfigLoader): here. """ +kv_pattern = re.compile(r'[A-Za-z]\w*(\.\w+)*\=.+') +macro_pattern = re.compile(r'\-\-\w+(\-\w)*') class KeyValueConfigLoader(CommandLineConfigLoader): """A config loader that loads key value pairs from the command line. @@ -312,7 +315,7 @@ class KeyValueConfigLoader(CommandLineConfigLoader): ipython Global.profile="foo" InteractiveShell.autocall=False """ - def __init__(self, argv=None, classes=None): + def __init__(self, argv=None, shortnames=None, macros=None): """Create a key value pair config loader. Parameters @@ -321,9 +324,14 @@ class KeyValueConfigLoader(CommandLineConfigLoader): A list that has the form of sys.argv[1:] which has unicode elements of the form u"key=value". If this is None (default), then sys.argv[1:] will be used. - classes : (list, tuple) of Configurables - A sequence of Configurable classes that will be used to map - shortnames to longnames. + shortnames : dict + A dict of aliases for configurable traits. + Keys are the short aliases, Values are the resolved trait. + Of the form: `{'shortname' : 'Configurable.trait'}` + macros : dict + A dict of macros, keyed by str name. Vaues can be Config objects, + dicts, or "key=value" strings. If Config or dict, when the macro + is triggered, The macro is loaded as `self.config.update(m)`. Returns ------- @@ -340,12 +348,11 @@ class KeyValueConfigLoader(CommandLineConfigLoader): """ if argv is None: argv = sys.argv[1:] - if classes is None: - classes = () self.argv = argv - self.classes = classes + self.shortnames = shortnames or {} + self.macros = macros or {} - def load_config(self, argv=None, classes=None): + def load_config(self, argv=None, shortnames=None, macros=None): """Parse the configuration and generate the Config object. Parameters @@ -354,37 +361,28 @@ class KeyValueConfigLoader(CommandLineConfigLoader): A list that has the form of sys.argv[1:] which has unicode elements of the form u"key=value". If this is None (default), then self.argv will be used. - classes : (list, tuple) of Configurables - A sequence of Configurable classes that will be used to map - shortnames to longnames. + shortnames : dict + A dict of aliases for configurable traits. + Keys are the short aliases, Values are the resolved trait. + Of the form: `{'shortname' : 'Configurable.trait'}` + macros : dict + A dict of macros, keyed by str name. Vaues can be Config objects, + dicts, or "key=value" strings. If Config or dict, when the macro + is triggered, The macro is loaded as `self.config.update(m)`. """ from IPython.config.configurable import Configurable self.clear() if argv is None: argv = self.argv - if classes is None: - classes = self.classes - - # Create the mapping between shortnames and longnames. - shortnames = {} - for cls in classes: - if issubclass(cls, Configurable): - sn = cls.class_get_shortnames() - # Check for duplicate shortnames and raise if found. - for k, v in sn.items(): - if k in shortnames: - raise KeyError( - 'Duplicate shortname: both %s and %s use the shortname: "%s"' %\ - (v, shortnames[k], k) - ) - shortnames.update(sn) + if shortnames is None: + shortnames = self.shortnames + if macros is None: + macros = self.macros for item in argv: - pair = tuple(item.split("=")) - if len(pair) == 2: - lhs = pair[0] - rhs = pair[1] + if kv_pattern.match(item): + lhs,rhs = item.split('=',1) # Substitute longnames for shortnames. if lhs in shortnames: lhs = shortnames[lhs] @@ -400,9 +398,26 @@ class KeyValueConfigLoader(CommandLineConfigLoader): # it succeeds. If it still fails, we let it raise. exec_str = 'self.config.' + lhs + '="' + rhs + '"' exec exec_str in locals(), globals() + elif macro_pattern.match(item): + # trim leading '--' + m = item[2:] + macro = macros.get(m, None) + if macro is None: + raise ValueError("Unrecognized argument: %r"%item) + macro = macros[m] + if isinstance(macro, basestring): + # macro is simply a 'Class.trait=value' string + exec_str = 'self.config.' + macro + exec exec_str in locals(), globals() + elif isinstance(macro, (dict, Configurable)): + # update self.config with Config: + self.config.update(macros[m]) + else: + raise ValueError("Invalid macro: %r"%macro) + else: + raise ValueError("Invalid argument: %r"%item) return self.config - class ArgParseConfigLoader(CommandLineConfigLoader): """A loader that uses the argparse module to load from the command line.""" diff --git a/IPython/config/tests/test_application.py b/IPython/config/tests/test_application.py index 2053d99..95d1d28 100644 --- a/IPython/config/tests/test_application.py +++ b/IPython/config/tests/test_application.py @@ -35,14 +35,14 @@ from IPython.utils.traitlets import ( class Foo(Configurable): - i = Int(0, config=True, shortname='i', help="The integer i.") - j = Int(1, config=True, shortname='j', help="The integer j.") - name = Unicode(u'Brian', config=True, shortname='name', help="First name.") + i = Int(0, config=True, help="The integer i.") + j = Int(1, config=True, help="The integer j.") + name = Unicode(u'Brian', config=True, help="First name.") class Bar(Configurable): - enabled = Bool(True, config=True, shortname="enabled", help="Enable bar.") + enabled = Bool(True, config=True, help="Enable bar.") class MyApp(Application): @@ -54,6 +54,15 @@ class MyApp(Application): config_file = Unicode(u'', config=True, shortname="config_file", help="Load this config file") + shortnames = dict(i='Foo.i',j='Foo.j',name='Foo.name', + enabled='Bar.enabled', log_level='MyApp.log_level') + + macros = dict(enable='Bar.enabled=True', disable='Bar.enabled=False') + + macro_help = dict( + enable="""Enable bar""", + disable="""Disable bar""" + ) def init_foo(self): self.foo = Foo(config=self.config) @@ -88,3 +97,12 @@ class TestApplication(TestCase): self.assertEquals(app.foo.j, 10) self.assertEquals(app.bar.enabled, False) + def test_macro(self): + app = MyApp() + app.parse_command_line(["--disable"]) + app.init_bar() + self.assertEquals(app.bar.enabled, False) + app.parse_command_line(["--enable"]) + app.init_bar() + self.assertEquals(app.bar.enabled, True) + diff --git a/IPython/config/tests/test_configurable.py b/IPython/config/tests/test_configurable.py index 7fc63ff..3c28b7a 100644 --- a/IPython/config/tests/test_configurable.py +++ b/IPython/config/tests/test_configurable.py @@ -40,26 +40,26 @@ from IPython.config.loader import Config class MyConfigurable(Configurable): - a = Int(1, config=True, shortname="a", help="The integer a.") - b = Float(1.0, config=True, shortname="b", help="The integer b.") + a = Int(1, config=True, help="The integer a.") + b = Float(1.0, config=True, help="The integer b.") c = Str('no config') mc_help=u"""MyConfigurable options ---------------------- -MyConfigurable.a : Int (shortname=a) +MyConfigurable.a : Int The integer a. -MyConfigurable.b : Float (shortname=b) +MyConfigurable.b : Float The integer b.""" class Foo(Configurable): - a = Int(0, config=True, shortname="a", help="The integer a.") + a = Int(0, config=True, help="The integer a.") b = Str('nope', config=True) class Bar(Foo): - b = Str('gotit', config=False, shortname="b", help="The string b.") - c = Float(config=True, shortname="c", help="The string c.") + b = Str('gotit', config=False, help="The string b.") + c = Float(config=True, help="The string c.") class TestConfigurable(TestCase): @@ -135,14 +135,6 @@ class TestConfigurable(TestCase): self.assertEquals(c.b, 'and') self.assertEquals(c.c, 20.0) - def test_shortnames(self): - sn = MyConfigurable.class_get_shortnames() - self.assertEquals(sn, {'a': 'MyConfigurable.a', 'b': 'MyConfigurable.b'}) - sn = Foo.class_get_shortnames() - self.assertEquals(sn, {'a': 'Foo.a'}) - sn = Bar.class_get_shortnames() - self.assertEquals(sn, {'a': 'Bar.a', 'c': 'Bar.c'}) - def test_help(self): self.assertEquals(MyConfigurable.class_get_help(), mc_help) diff --git a/IPython/config/tests/test_loader.py b/IPython/config/tests/test_loader.py index 8a0dcf3..f5439ce 100755 --- a/IPython/config/tests/test_loader.py +++ b/IPython/config/tests/test_loader.py @@ -124,23 +124,6 @@ class TestKeyValueCL(TestCase): self.assertEquals(config.Foo.Bam.value, range(10)) self.assertEquals(config.D.C.value, 'hi there') - def test_shortname(self): - class Foo(Configurable): - i = Int(0, config=True, shortname="i") - s = Unicode('hi', config=True, shortname="s") - cl = KeyValueConfigLoader() - config = cl.load_config(["i=20", "s=there"], classes=[Foo]) - self.assertEquals(config.Foo.i, 20) - self.assertEquals(config.Foo.s, "there") - - def test_duplicate(self): - class Foo(Configurable): - i = Int(0, config=True, shortname="i") - class Bar(Configurable): - i = Int(0, config=True, shortname="i") - cl = KeyValueConfigLoader() - self.assertRaises(KeyError, cl.load_config, ["i=20", "s=there"], classes=[Foo, Bar]) - class TestConfig(TestCase): diff --git a/docs/examples/core/appconfig.py b/docs/examples/core/appconfig.py index 44bb74a..ad23b30 100644 --- a/docs/examples/core/appconfig.py +++ b/docs/examples/core/appconfig.py @@ -62,6 +62,15 @@ class MyApp(Application): classes = List([Bar, Foo]) config_file = Unicode(u'', config=True, shortname="config_file", help="Load this config file") + + shortnames = dict(i='Foo.i',j='Foo.j',name='Foo.name', + enabled='Bar.enabled') + + macros = dict(enable='Bar.enabled=True', disable='Bar.enabled=False') + macro_help = dict( + enable="""Set Bar.enabled to True""", + disable="""Set Bar.enabled to False""" + ) def init_foo(self): # Pass config to other classes for them to inherit the config.