Installation

Climax is installed with pip:

$ pip install climax

Getting Started

The following example should give you a pretty good idea of how climax works:

import climax

@climax.command()
@climax.argument('name', help='the name to repeat')
@climax.argument('--count', type=int, help='how many times to repeat')
def repeat(count, name):
    """This silly program repeats a name the given number of times."""
    for i in range(count):
        print(name)

if __name__ == '__main__':
    repeat()

In the script above, the arguments to the @climax.argument decorator are anything you would send to the ArgumentParser.add_argument method. In fact, climax passes these arguments to it untouched. The @climax.command decorator takes optional arguments, which are passed to the ArgumentParser constructor. For example, to set a custom program name, you can pass prog='my_command_name'.

When you run the above script, you get a functional command line parser:

$ python repeat.py
usage: repeat.py [-h] [--count COUNT] name

The --help option is automatically generated by argparse, from the information passed on the decorators:

$ python repeat.py --help
usage: repeat.py [-h] [--count COUNT] name

This silly program repeats a name the given number of times.

positional arguments:
  name           the name to repeat

optional arguments:
  -h, --help     show this help message and exit
  --count COUNT  how many times to repeat

If you provide valid arguments, then the command function runs:

$ python repeat.py --count 3 foo
foo
foo
foo

And if anything in the command line is incorrect, you get an error:

$ python repeat.py --count not-a-number foo
usage: repeat.py [-h] [--count COUNT] name
repeat.py: error: argument --count: invalid int value: 'not-a-number'

Building Command Groups

One of the nicest features of argparse is the ability to build complex command lines by grouping multiple commands under a single top-level parser. Climax support these easily:

import climax

@climax.group()
def main():
    pass

@main.command()
@climax.argument('values', type=int, nargs='+', help='sequence of numbers to add')
def add(values):
    """add numbers"""
    print(sum(values))

@main.command()
@climax.argument('values', type=int, nargs='+', help='sequence of numbers to average')
def avg(values):
    """average numbers"""
    print(sum(values) / len(values))

if __name__ == '__main__':
    main()

Note that to link a command to its parent group, the command decorator is obtained from the group function (i.e. @main.command instead of @climax.command).

Without any arguments, this script generates the following output:

$ python sumavg.py
usage: sumavg.py [-h] {add,avg} ...
sumavg.py: error: too few arguments

The --help now generates information about the group of commands:

$ python sumavg.py --help
usage: sumavg.py [-h] {add,avg} ...

positional arguments:
  {add,avg}
    add       add numbers
    avg       average numbers

optional arguments:
  -h, --help  show this help message and exit

And each command generates its own help messages as well:

$ python sumavg.py add
usage: sumavg.py add [-h] values [values ...]
sumavg.py add: error: the following arguments are required: values

$ python sumavg.py add --help
usage: sumavg.py add [-h] values [values ...]

positional arguments:
  values      sequence of numbers to add

optional arguments:
  -h, --help  show this help message and exit

Other Useful Features

Options vs. Arguments

Argparse does not make a distinction between options and arguments, positional and optional arguments are considered arguments. In climax, the @climax.argument and @climax.option decorators are equivalent, so they can be used according to your preference.

Password Prompts

Argparse does not provide any facility to enter passwords securely. Since this is a common need of scripts, climax includes a custom argparse action that adds this functionality:

import climax

@climax.command()
@climax.argument('--username', '-u', help='your username')
@climax.argument('--password', '-p', action=climax.PasswordPrompt,
                 help='prompt for your password')
def login(username, password):
    """Login example."""
    print(username, password)

if __name__ == '__main__':
    login()

When you run this script, the getpass function from the Python standard library is invoked to prompt for the password without echoing what you type:

$ python login.py -u john -p
Password:

Contexts

In the command group example above, there is a function associated with the group, called main. If arguments are defined at this level, they will apply to all the commands in the group. When a command is invoked, climax first calls the group function with its arguments, and then calls the appropriate command function.

Consider, for example, a --verbose option, which applies to all commands in a group:

@climax.group()
@climax.argument('--verbose', action='store_true')
def main(verbose):
    return {'verbose': verbose}

After the group function processes its arguments, it may need to communicate some state to the command function that will run after it. For this purpose, the group function can return a dictionary with values that will be sent as arguments into the command function, in addition to the arguments generated by argparse.

To support verbosity, the sum function can then be coded as follows:

@main.command()
@climax.argument('values', type=int, nargs='+', help='sequence of numbers to add')
def add(values, verbose):
    """add numbers"""
    if verbose:
        print('The input values are: ', str(values))
    print(sum(values))

Return Values

You have seen in the previous section that the return value from a group function is the context that is passed as arguments to the command function. A command function can also return a value, which is returned to the caller:

import climax

@climax.command()
@climax.argument('--count', type=int, help='how many times to repeat')
@climax.argument('name', help='the name to repeat')
def repeat(count, name):
    """This silly program repeats a name the given number of times."""
    for i in range(count):
        print(name)
    return count

if __name__ == '__main__':
    result = main()
    # result now has the return value from the command

Parents

Argparse supports the concept of “parents”, which allows a command to inherit arguments from another command, which is designated as a parent. The purpose of this feature is to avoid duplication when a common set of arguments needs to be applied to several commands.

With climax, the parent feature is available through a parent decorator:

import climax

@climax.parent()
@climax.argument('--count', type=int, help='how many times to repeat')
def repeat()
    pass

@climax.group()
def main():
    pass

@main.command(parents=[repeat])
def foo(count):
    for i in range(count):
        print('foo')

@main.command(parents=[repeat])
@climax.argument('name', help='the name to repeat')
def bar(name, count):
    for i in range(count):
        print(name)

if __name__ == '__main__':
    main()

In this example, both the foo and bar commands accept --count as argument. The function that handles a command that has parents will receive its own arguments combined with those of the parents.

Optional Commands

In Python 3.2 and older, argparse requires that a command name is specified when using groups. But argparse versions that ship with Python 3.3 and newer lift that requirement due to a bug that inadvertently slipped. This makes it possible to specify a command line in which no command from the group is selected, something that can sometimes be useful.

If you are using one of the newer Python versions, climax makes group commands required by default, like in the older Python releases. This gives a consistent behavior. To make commands in a group optional, the group can be given the required=False argument:

import climax

@climax.group(required=False)
def main():
    print('this is main')

@main.command()
def cmd()
    print('this is cmd')

With this example, the following two commands are both valid:

$ python main.py cmd
this is main
this is cmd

$ python main.py
this is main

Without setting required=False, the second command would return an error. Note that optional commands do not work in Python releases before 3.3.

Recursive Groups

Argparse supports multiple levels of commands and sub-commands. In climax, multiple layers of commands can be built using the familiar decorator syntax. Consider the following example:

@climax.group()
def main():
    pass

@main.group()
def sub1():
    pass

@sub1.command()
def sub1a():
    pass

@sub1.command()
def sub1b():
    pass

@main.command()
def sub2():
    pass

Integration with native argparse parsers

Climax’s use of argparse is not magical. In fact, it is possible to attach a regular argparse parser as a command in a climax group, by passing a parser argument to the @command decorator:

@climax.group()
def grp():
    pass

parser = argparse.ArgumentParser('cmd1.py')
parser.add_argument('--repeat', type=int)
parser.add_argument('name')

@grp.command(parser=parser)
def cmd1(repeat, name):
    pass

Or directly to the main command:

import climax

parser = argparse.ArgumentParser('cmd.py')
parser.add_argument('--repeat', type=int)
parser.add_argument('name')


@climax.command(parser=parser)
def cmd(repeat, name):
    pass

The reverse is also possible. If you need to obtain the argparse parser generated by climax to integrate it with another parser or to make custom modifications to it, you can simply obtain it by invoking the parser attribute on the corresponding function. In the above example, grp.parser returns a fully built and ready to use parser.