Command-Line Helpers

This package provides a few handy additions to the click command-line interface (CLI) library, allowing one to build even more powerful CLIs.

Verbosity Option

The clapper.click.verbosity_option() click decorator allows one to control the logging-level of a pre-defined :py:class:logging.Logger. Here is an example usage.

import clapper.click
import logging

# retrieve the base-package logger
logger = logging.getLogger(__name__.split(".", 1)[0])


@clapper.click.verbosity_option(logger)
def cli(verbose):
    pass

The verbosity option binds the command-line (-v) flag usage to setting the logging.Logger level by calling logging.Logger.setLevel() with the appropriate logging level, mapped as such:

  • 0 (the user has provide no -v option on the command-line): logger.setLevel(logging.ERROR)

  • 1 (the user provided a single -v): logger.setLevel(logging.WARNING)

  • 2 (the user provided the flag twice, -vv): logger.setLevel(logging.INFO)

  • 3 (the user provide the flag thrice or more, -vvv): logger.setLevel(logging.DEBUG)

Note

If you do not care about the verbose parameter in your command and only rely on the decorator to set the logging level, you can set expose_value to False:

@clapper.click.verbosity_option(logger, expose_value=False)
def cli():
    pass

Config Command

The clapper.click.ConfigCommand is a type of click.Command in which declared CLI options may be either passed via the command-line, or loaded from a Experimental Configuration Options. It works by reading the Python configuration file and filling up option values pretty much as click would do, with one exception: CLI options can now be of any Pythonic type.

To implement this, a CLI implemented via clapper.click.ConfigCommand may not declare any arguments, only options. All arguments are interpreted as configuration files, from where option values will be set, in order. Any type of configuration resource can be provided (file paths, python modules or entry-points). Command-line options take precedence over values set in configuration files. The order of configuration files matters, and the final values for CLI options follow the same rules as in Chain Loading.

Options that may be read from configuration files must also be marked with the custom click-type clapper.click.ResourceOption.

Here is an example usage of this class:

Listing 4 Example CLI with config-file support
# SPDX-FileCopyrightText: Copyright © 2022 Idiap Research Institute <contact@idiap.ch>
#
# SPDX-License-Identifier: BSD-3-Clause
"""An example script to demonstrate config-file option readout."""

# To improve loading performance, we recommend you only import the very
# essential packages needed to start the CLI.  Defer all other imports to
# within the function implementing the command.

import click

from clapper.click import ConfigCommand, ResourceOption, verbosity_option
from clapper.logging import setup

logger = setup(__name__.split(".", 1)[0])


@click.command(
    context_settings={
        "show_default": True,
        "help_option_names": ["-?", "-h", "--help"],
    },
    # if configuration 'modules' must be loaded from package entry-points,
    # then must search this entry-point group:
    entry_point_group="test.app",
    cls=ConfigCommand,
    epilog="""\b
Examples:

  $ test_app -vvv --integer=3
""",
)
@click.option("--integer", type=int, default=42, cls=ResourceOption)
@click.option("--flag/--no-flag", default=False, cls=ResourceOption)
@click.option("--str", default="foo", cls=ResourceOption)
@click.option(
    "--choice",
    type=click.Choice(["red", "green", "blue"]),
    cls=ResourceOption,
)
@verbosity_option(logger=logger)
@click.version_option(package_name="clapper")
@click.pass_context
def main(ctx, **_):
    """Test our Click interfaces."""
    # Add imports needed for your code here, and avoid spending time loading!

    # In this example, we just print the loaded options to demonstrate loading
    # from config files actually works!
    for k, v in ctx.params.items():
        if k in ("dump_config", "config"):
            continue
        click.echo(f"{k}: {v}")


if __name__ == "__main__":
    main()

If a configuration file is setup like this:

Listing 5 Example configuration file for the CLI above
# SPDX-FileCopyrightText: Copyright © 2022 Idiap Research Institute <contact@idiap.ch>
#
# SPDX-License-Identifier: BSD-3-Clause

integer = 1000
flag = True
choice = "blue"
str = "bar"  # noqa: A001

Then end result would be this:

$ python example_cli.py example_options.py
verbose: 0
integer: 1000
flag: True
str: bar
choice: blue

Notice that configuration options on the command-line take precedence:

$ python example_cli.py --str=baz example_options.py
verbose: 0
str: baz
integer: 1000
flag: True
choice: blue

Configuration options can also be loaded from package entry-points named test.app. To do this, a package setup would have to contain a group named test.app, and list entry-point names which point to modules containing variables that can be loaded by the CLI application. For example, would a package declare this entry-point:

entry_points={
    # some test entry_points
    'test.app': [
        'my-config = path.to.module.config',
        ...
    ],
},

Then, the application shown above would also be able to work like this:

python example_cli.py my-config

Options with type clapper.click.ResourceOption may also point to individual resources (specific variables on python modules). This may be, however, a more seldomly used feature. Read the class documentation for details.

Aliased Command Groups

When designing an CLI with multiple subcommands, it is sometimes useful to be able to shorten command names. For example, being able to use git ci instead of git commit, is a form of aliasing. To do so in click CLIs, it suffices to subclass all command group instances with clapper.click.AliasedGroup. This should include groups and subgroups of any depth in your CLI. Here is an example usage:

Listing 6 Example CLI with group aliasing support
# SPDX-FileCopyrightText: Copyright © 2022 Idiap Research Institute <contact@idiap.ch>
#
# SPDX-License-Identifier: BSD-3-Clause
"""An example script to demonstrate config-file option readout."""

# To improve loading performance, we recommend you only import the very
# essential packages needed to start the CLI.  Defer all other imports to
# within the function implementing the command.

import click

import clapper.click


@click.group(cls=clapper.click.AliasedGroup)
def main():
    """Declare main command-line application."""
    pass


@main.command()
def push():
    """Push subcommand."""
    click.echo("push was called")


@main.command()
def pop():
    """Pop subcommand."""
    click.echo("pop was called")


if __name__ == "__main__":
    main()

You may then shorten the command to be called such as this:

$ python example_alias.py pu
push was called

Experiment Options (Config) Command-Group

When building complex CLIs in which support for configuration is required, it may be convenient to provide users with CLI subcommands to display configuration resources (examples) shipped with the package. To this end, we provide an easy to plug click.Group decorator that attaches a few useful subcommands to a predefined CLI command, from your package. Here is an example on how to build a CLI to do this:

Listing 7 Implementation a command group to affect the RC file of an application.
# SPDX-FileCopyrightText: Copyright © 2022 Idiap Research Institute <contact@idiap.ch>
#
# SPDX-License-Identifier: BSD-3-Clause

from clapper.click import config_group
from clapper.logging import setup

logger = setup(__name__.split(".", 1)[0])


@config_group(logger=logger, entry_point_group="clapper.test.config")
def main(**kwargs):
    """Use this command to list/describe/copy package config resources."""
    pass


if __name__ == "__main__":
    main()

Here is the generated command-line:

$ python example_config.py --help
Usage: example_config.py [OPTIONS] COMMAND [ARGS]...

  Use this command to list/describe/copy package config resources.

Options:
  -v, --verbose  Increase the verbosity level from 0 (only error and critical)
                 messages will be displayed, to 1 (like 0, but adds warnings),
                 2 (like 1, but adds info messages), and 3 (like 2, but also
                 adds debugging messages) by adding the --verbose option as
                 often as desired (e.g. '-vvv' for debug).  [default: 0;
                 0<=x<=3]
  -h, --help     Show this message and exit.

Commands:
  copy      Copy a specific configuration resource so it can be modified...
  describe  Describe a specific configuration resource.
  list      List installed configuration resources.

You may try to use that example application like this:

# lists all installed resources in the entry-point-group
# "clapper.test.config"
$ python doc/example_config.py list
module: tests.data
    complex
    complex-var
    first
    first-a
    first-b
    second
    second-b
    second-c
    verbose-config

# describes a particular resource configuration
# Adding one or more "-v" (verbosity) options affects
# what is printed.
$ python doc/example_config.py describe "complex" -vv
Configuration: complex
Python Module: tests.data.complex

Contents:
cplx = dict(
    a="test",
    b=42,
    c=3.14,
    d=[1, 2, 37],
)

# copies the module pointed by "complex" locally (to "local.py")
# for modification and testing
$ python doc/example_config.py copy complex local.py
$ cat local.py
cplx = dict(
    a="test",
    b=42,
    c=3.14,
    d=[1, 2, 37],
)

Global Configuration (RC) Command-Group

When building complex CLIs in which support for global configuration is required, it may be convenient to provide users with CLI subcommands to display current values, set or get the value of specific configuration variables. For example, the git CLI provides the git config command that fulfills this task. Here is an example on how to build a CLI to affect your application’s global RC file:

Listing 8 Implementation a command group to affect the RC file of an application.
# SPDX-FileCopyrightText: Copyright © 2022 Idiap Research Institute <contact@idiap.ch>
#
# SPDX-License-Identifier: BSD-3-Clause

from clapper.click import user_defaults_group
from clapper.logging import setup
from clapper.rc import UserDefaults

logger = setup(__name__.split(".", 1)[0])
rc = UserDefaults("myapp.toml", logger=logger)


@user_defaults_group(logger=logger, config=rc)
def main(**kwargs):
    """Use this command to affect the global user defaults."""
    pass


if __name__ == "__main__":
    main()

Here is the generated command-line:

$ python example_defaults.py --help
Usage: example_defaults.py [OPTIONS] COMMAND [ARGS]...

  Use this command to affect the global user defaults.

Options:
  -v, --verbose  Increase the verbosity level from 0 (only error and critical)
                 messages will be displayed, to 1 (like 0, but adds warnings),
                 2 (like 1, but adds info messages), and 3 (like 2, but also
                 adds debugging messages) by adding the --verbose option as
                 often as desired (e.g. '-vvv' for debug).  [default: 0;
                 0<=x<=3]
  -h, --help     Show this message and exit.

Commands:
  get   Print a key from the user-defaults file.
  rm    Remove the given key from the configuration file.
  set   Set the value for a key on the user-defaults file.
  show  Show the user-defaults file contents.

You may try to use that example application like this:

$ python example_defaults.py set foo 42
$ python example_defaults.py set bla.float 3.14
$ python example_defaults.py get bla
{'float': 3.14}
$ python example_defaults.py show
foo = 42

[bla]
float = 3.14
$ python example_defaults.py rm bla
$ python example_defaults.py show
foo = 42
$

Multi-package Command Groups

You may have to write parts of your CLI in different software packages. We recommend you look into the Click-Plugins extension module as means to implement this in a Python-oriented way, using the package entry-points (plugin) mechanism.

Log Parameters

The clapper.click.log_parameters() click method allows one to log the parameters used within the current click context and their value for debuging purposes. Here is an example usage.

import clapper.click
import logging

# retrieve the base-package logger
logger = logging.getLogger(__name__)


@clapper.click.verbosity_option(logger, short_name="vvv")
def cli(verbose):
   clapper.click.log_parameters(logger)

A pre-defined logging.Logger have to be provided and, optionally, a list of parameters to ignore can be provided as well, as a Tuple.