Tuesday, March 05, 2013

Named Arguments for Configman

I'm proposing a small modification to Configman to add named arguments to its Swiss Army Knife feature set. Neglected in the first versions of Configman, I think this proposal will be very useful. So what I'm I talking about?
    $ some_app.py  --help
    usage:
        some_app.py [OPTIONS]...  arg1 [ arg2 [ arg3 ]]

It's the arg1, arg2, arg3 that I'm interested in here. Normally, you'd access these with sys.args without configman or config.args with configman. They come to the programmer as an uninterpreted list of strings. I'm proposing treating them just like switches. We should be able to define what is expected: the position, defaults, conversion functions, actions to take, just like the command line switches. The end result to the programmer should be something that looks like this:
    config = config_manager.get_config()
    print config.arg1, config.arg2, config.arg3

In other words, from the programmers perspective, they're just values passed into the program with the same access method and priority as command line switches. In fact, with a minor change to the configman Option object, they can be used to specify both switches and arguments:
    n.add_option(
        name='filename',
        doc='the name of the file',
        default=None,
        is_argument=True
    )
    n.add_option(
        name='action',
        doc='the action to take on the file',
        default='echo',
    )

Within the program, this could be accessed as:
    config = config_manager.get_config()
    with open(config.filename) as fp:
        do_something_interesting(fp)

From the users perspective, the command line can be used like this:
    $ some_app.py --help
    usage:
        some_app.py [OPTIONS]... filename
    OPTIONS:
        --action    the action to take on the file (default: echo)
        --filename  the name of the file

    $ some_app.py my_file.txt
    contents of my file

    $ some_app.py --action=upper my_file.txt
    CONTENTS OF MY FILE

    $ some_app.py --filename=my_file.txt --action=backwards
    elif ym fo stnetnoc

Notice that the actions can be specified as either positional arguments or as switches. If you do one then the other is automatically disallowed to prevent conflicts. By treating positional arguments in the same manner as switches, we get the benefit of configman's dependency injection. Say the first positional argument is the action and that corresponds with the name of a function. Because we specified the converter for the action argument to load a matching Python object from the scope, config.action will be a callable function:
    def echo(x):
        print x
    
    def backwards(x):
        print x[::-1]
    
    def upper(x):
        print x.upper()
    
    n = Namespace()
    n.add_option(
        'action',
        default=None,
        doc='the action to take [echo, backwards, upper]',
        short_form='a',
        is_argument=True,
        from_string_converter=class_converter
    )
    n.add_option(
        'text',
        default='Socorro Forever',
        doc='the text input value',
        short_form='t',
        is_argument=True,
    )
    c = ConfigurationManager(
        n,
        app_name='demo1',
        app_description=__doc__
    )
    try:
        config = c.get_config()
        config.action(config.text)
    except AttributeError, x:
        print "%s is not a valid command"
    except TypeError:
        print "you must specify an action"

Which will yield this user experience:
    $ demo1.py --help
    usage:
        demo1.py [OPTIONS]... action [ text ]
    OPTIONS:
        --action  the action to take [echo, backwards, upper]
        --text    the text input value
                  default: "Socorro Forever"

Notice that because the action has no default, action is required, there are no brackets around it in help. However, if you specify it as a switch, the necessity to use it as an argument goes away.
    $ demo1.py backwards "Configman is pretty cool"
    looc ytterp si namgifnoC
    $ demo1.py "Socorro uses Configman" --action=upper
    SOCORRO USES CONFIGMAN

I think this is pretty cool because it makes subcommands and subcommand help simple. The list of options and additional arguments are loaded dynamically from the classes (or functions) that the user specifies on the command line.
    $ socorro.py processor --help
    usage:
        socorro.py [OPTIONS]... command
    OPTIONS:
        --command   the socorro subsystem to start (default: processor)
        -- ...   all the options for processor

    $ socorro.py monitor --help
    usage:
        socorro.py [OPTIONS]... command
    OPTIONS:
        --command   the socorro subsystem to start (default: monitor)
        -- ...   all the options for monitor