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.