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
-voption 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:
# 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:
# 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:
# 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:
# 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:
# 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.