Usage

Defining commands

A command is a function decorated with @command. cltoolbox extracts as much as information as possible from the function’s docstring and function signature.

The argparse library is configured by cltoolbox from three different sources.

  1. Function signature
    • each argument name

    • each argument type (from type hints)

  2. Function docstring (Sphinx, Google, Numpy, or epydoc formats)
    • function help which is the first paragraph of the docstring and shouldn’t be longer than one line.

    • function description is taken from the remaining paragraphs up until the documentation of arguments.

    • each argument help

  3. The cltoolbox.arg decorator which overrides or appends to any of the information found in the function signature or docstring.

Note that the argument types in the function docstring are ignored and do not override the type hints found in the function signature.

For example, this program uses Sphinx formatted docstring:

from cltoolbox import command, main

@command
def cmd(foo, bar):
    """Here stands the help.

    And here the description of this useless command.

    :param foo: Well, the first arg.
    :param bar: Obviously the second arg. Nonsense."""

    print(foo, bar)

if __name__ == "__main__":
    main()

The cltoolbox will take the previous function and docstring to create the following documentation available at the command line.

$ python command.py -h
usage: command.py [-h] {cmd} ...

positional arguments:
  {cmd}
    cmd       Here stands the help.

optional arguments:
  -h, --help  show this help message and exit
$ python command.py cmd -h
usage: command.py cmd [-h] foo bar

And here the description of this useless command.

positional arguments:
  foo         Well, the first arg.
  bar         Obviously the second arg. Nonsense.

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

Overriding arguments with @arg

You may need to specify some argument to argparse, and it is not possible to include in the docstring or function signature. cltoolbox provides the @arg decorator to accomplish this. Its signature is as follows: @arg(arg_name, *args, **kwargs), where arg_name must be among the function’s arguments, while the remaining arguments will be directly passed to argparse.add_argument().

Note that this decorator will override other arguments that cltoolbox inferred either from the function signature or from the docstring.

The following are the argparse.add_argument() positional and keyword arguments:

Positional (if used must be first in @arg

name or flags

Either a name or a list of option strings, e.g. foo or -f, –foo.

Keyword arguments

action

The basic type of action to be taken when this argument is encountered at the command line.

nargs

The number of command-line arguments that should be consumed.

const

A constant value required by some action and nargs selections.

default

The value produced if the argument is absent from the command line and if it is absent from the namespace object.

type

The type to which the command-line argument should be converted.

choices

A container of the allowable values for the argument.

required

Whether or not the command-line option may be omitted (optionals only).

help

A brief description of what the argument does.

metavar

A name for the argument in usage messages.

dest

The name of the attribute to be added to the object returned by parse_args().

Long and short options (flags)

The @arg decorator is useful for allowing long and short options for the keyword arguments.

Example:

from cltoolbox import command, main, arg

@command
@arg("spam", "--spam", "-s")
def ex(foo, b=None, spam=None):
    """Nothing interesting.

    :param foo: Bla bla.
    :param b: A little flag.
    :param spam: Spam spam spam spam."""

    print((foo, b, spam))

if __name__ == "__main__":
    main()

Usage:

$ python short_options.py ex -h
usage: short_options.py ex [-h] [-b B] [--spam SPAM] foo

positional arguments:
  foo                   Bla bla.

optional arguments:
  -h, --help            show this help message and exit
  -b B                  A little flag.
  --spam SPAM, -s SPAM  Spam spam spam spam.
$ python short_options.py ex 2
('2', None, None)
$ python short_options.py ex 2 -b 8
('2', '8', None)
$ python short_options.py ex 2 -b 8 -s 9
('2', '8', '9')
$ python short_options.py ex 2 -b 8 --spam 9
('2', '8', '9')

How default arguments are handled

If an argument has a default, then cltoolbox takes it as an optional argument, while those which do not have a default are interpreted as positional arguments. Here are the actions taken by cltoolbox when a default argument is encountered:

Default argument type

What cltoolbox specifies in add_argument()

bool

action store_true or store_false is added

list

action append is added.

int

type int() is added.

float

type float() is added.

str

type str() is added.

So, for example, if a default argument is an integer, cltoolbox will automatically convert command line arguments to int():

from cltoolbox import command, main

@command
def po(a=2, b=3):
    print(a ** b)

if __name__ == "__main__":
    main()
$ python default_args.py po -h
usage: default_args.py po [-h] [-a A] [-b B]

optional arguments:
  -h, --help  show this help message and exit
  -a A
  -b B
$ python default_args.py po -a 4 -b 9
262144

Note that passing the arguments positionally does not work, because argparse expects optional args and a and b are already filled with defaults:

$ python default_args.py po
8
$ python default_args.py po 9 8
usage: default_args.py [-h] {po} ...
default_args.py: error: unrecognized arguments: 9 8

To overcome this, cltoolbox allows you to specify positional arguments’ types in the type hints, as explained in the next section.

Adding type

This is especially useful for positional arguments, but it can be used for all type of arguments.

Adding type in the signature

The cltoolbox can use type annotations to convert argument types.

Simple usage:

from cltoolbox import command, main, arg

@command
@arg("mod", "--mod", "-m")
def pow(a:float, b:float, mod:int=None):
    """Mimic Python's pow() function.

    :param a: The base.
    :param b: The exponent.
    :param mod: Modulus."""

    if mod is not None:
        print((a ** b) % mod)
    else:
        print(a ** b)

if __name__ == "__main__":
    main()
$ python types.py pow -h
usage: types.py pow [-h] [-m <int>] a b

Mimic Python's pow() function.

positional arguments:
  a                     The base.
  b                     The exponent.

optional arguments:
  -h, --help            show this help message and exit
  -m <int>, --mod <int>
                        Modulus.
$ python types.py pow 5 8
390625.0
$ python types.py pow 4.5 8.3
264036.437449

Since type annotations can be any callable, this allows more flexibility to convert what is given on the command line.

$ python types.py pow 5 8 -m 8
1.0
from cltoolbox import command, main


# Note: don't actually do this.
def double_int(n):
    return int(n) * 2


@command
def dup(string, times: double_int):
    """
    Duplicate text.

    :param string: The text to duplicate.
    :param times: How many times to duplicate.
    """
    print(string * times)


if __name__ == "__main__":
    main()
$ python3 dup_type_hints.py dup "test " 2
test test test test
$ python3 dup_type_hints.py dup "test " foo
usage: dup_type_hints.py dup [-h] string times
dup_type_hints.py dup: error: argument times: invalid double_int value: 'foo'

@command Arguments

There are two special arguments to the @command() decorator to allow for special processing for the decorated function. The first argument, also available as keyword name='alias_name' will allow for an alias of the command. The second is only available as keyword formatter_class='argparse_formatter_class' to format the display of the docstring.

Aliasing Commands

A common use-case for this is represented by a function with underscores in it. Usually commands have dashes instead. So, you may specify the aliasing name to the @command() decorator, this way:

@command('very-powerful-cmd')
def very_powerful_cmd(arg, verbose=False):
    pass

And call it as follows:

$ python prog.py very-powerful-cmd 2 --verbose

Note that the original name will be discarded and won’t be usable.

Docstring Formats

There are three commonly accepted formats for docstrings. The Sphinx or Restructured Text (REST) is the Python default, and the other two common styles are numpy and google. cltoolbox will auto-detect the style used.

An example of using a Numpy formatted docstring in cltoolbox:

@command
def simple_numpy_docstring(arg1, arg2="string"):
    '''One line summary.

    Extended description.

    Parameters
    ----------
    arg1 : int
        Description of `arg1`
    arg2 : str
        Description of `arg2`

    Returns
    -------
    str
        Description of return value.
    '''
    return int(arg1) * arg2

An example of using a Google formatted docstring in cltoolbox:

@command
def simple_google_docstring(arg1, arg2="string"):
    '''One line summary.

    Extended description.

    Args:
      arg1(int): Description of `arg1`
      arg2(str): Description of `arg2`
    Returns:
      str: Description of return value.
    '''
    return int(arg1) * arg2

Formatter Class

For the help display there is the opportunity to use special formatters. Any argparse compatible formatter class can be used. There is an alternative formatter class available with cltoolbox that will display on ANSI terminals.

The ANSI formatter class has to be imported from cltoolbox and used as follows:

from cltoolbox.rst_text_formatter import RSTHelpFormatter

@command(formatter_class=RSTHelpFormatter)
def pow(a:float, b:float, mod:int=None):
    '''Mimic Python's pow() function.

    :param a: The base.
    :param b: The exponent.
    :param mod: Modulus.'''

    if mod is not None:
        print((a ** b) % mod)
    else:
        print(a ** b)

Shell autocompletion

The cltoolbox supports autocompletion via the optional dependency argcomplete. If that package is installed, cltoolbox detects it automatically without the need to do anything else.