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