solovyov.net

Opster

· · python, programming, opster

Two months ago I’ve released little command line parsing library for Python called opster (actually it was called finaloption then, but I’ve renamed it because of remark from native speaker ;-)). What’s the reason to write one more command line parser when Python already has getopt and optparse in standard library and not so long time ago argparse and optfunc were released?

Well… as usually, because I think that they are going wrong way and doing wrong things. Of course, IMO (but what matters if not opinion? :P).

It started to happen when Zed Shaw wrote big article on Python warts and mentioned that Lamson has much better command line parsing solution than argparse/optparse. I was interested in this topic a little at the time and I looked at the code. It would be lie to say that I liked it. In fact I thought that this is a heresy of the same level as optparse. ;-)

I’ve written in Twitter that it’s funny to say that Lamson has superior command system and got some amount of sarcasm from Zed and clear understanding that Zed see nothing bad when:

  • command functions should be defined in a single module
  • default settings are defined by calling separate function inside a command function
  • specifying option in command line with mistake wouldn’t raise error
  • formatting of help text on options is done by hands

So I thought that world needs Mercurial’s command system. ;) And I’ve rewritten it as library, keeping main idea.

Small example of usage:

from opster import command

@command(usage='[-l HOST] DIR')
def main(dirname,
         listen=('l', 'localhost', 'ip to listen on'),
         port=('p', 8000, 'port to listen on'),
         daemonize=('d', False, 'daemonize process'),
         pid_file=('', '', 'name of file to write process ID to')):
    '''Command with option declaration as keyword arguments

    Otherwise it's the same as previous command
    '''
    print locals()

if __name__ == '__main__':
    main()

I think that you should understand what’s going on here. For example, option is required to have long name (keyword argument name), possibly short name (using '' will discard short name), some default value and help string. Default value determines what should be done to incoming data:

  • string - nothing happens, incoming value will remain as string
  • int - incoming value is parsed by calling int()
  • list - incoming value is appended to the list
  • function is called with incoming value and output is used
  • True/False/None - option needs no argument, just switching default value in opposite value

After wrapping with @command your function main() can be called:

  • without arguments at all; it will parse sys.argv in this case
  • with argument named argv, which needs to be list of strings (same as sys.argv[1:])
  • with usual arguments/keyword arguments, which are defined in function

I think it may be not obvious that you will get clean values in your function (for example, port will contain value 8000), and not some strange three-tuples.

And you get such help for free:

piranha@gto ~/dev/misc/opster>./test_opts.py --help
test_opts.py [-l HOST] DIR

Command with option declaration as keyword arguments

    Otherwise it's the same as previous command

options:

 -l --listen     ip to listen on (default: localhost)
 -p --port       port to listen on (default: 8000)
 -d --daemonize  daemonize process
    --pid-file   name of file to write process ID to
 -h --help       show help

Furthermore, underscores in argument names are converted to hyphens to support conventions of command line. ;)

I should mention that option names (and subcommand names, if you’re using them) can be shortened: i.e. you can say --pi instead of --pid-file.

If I’m going to compare opster with something, this should be optfunc by Simon Willison. Most noticeable differences are syntax of command definitions and subcommand support. Actually optfunc has subcommand support, but it’s pretty incomplete.

Opster uses getopt inside to parse options and that’s the reason why it’s somewhat bigger than optfunc (which is essentially optparse wrapper). Opster’s internal API - options are list of four-tuples (short name, long name, default value, help string) - is exactly the same as Mercurial’s API for defining options. This means that such code will work (taken from test_cmd.py):

import opster

config_opts=[('c', 'config', 'webshops.ini', 'config file to use')]

@opster.command(options=config_opts)
def initdb(config):
    """Initialize database"""
    pass

@opster.command(options=config_opts)
def runserver(listen=('l', 'localhost', 'ip to listen on'),
              port=('p', 5000, 'port to listen on'),
              **opts):
    """Run development server"""
    print locals()

if __name__ == '__main__':
    opster.dispatch()

Interesting thing happens in definition of runserver, help and output of which looks like this:

piranha@gto ~/dev/misc/opster> ./test_cmd.py help runs
test_cmd.py runserver [OPTIONS]

Run development server

options:

 -l --listen  ip to listen on (default: localhost)
 -p --port    port to listen on (default: 5000)
 -c --config  config file to use (default: webshops.ini)
 -h --help    display help

piranha@gto ~/dev/misc/opster> ./test_cmd.py runs
{'port': 5000, 'opts': {'config': 'webshops.ini'}, 'listen': 'localhost'}

You can factor out common options and pass them to @command decorator, keeping your pants DRY. ;-)

So… Read documentation and use it! :) Any feedback, questions, suggestions and patches are highly welcome. ;-)