Reputation: 115
I am trying to construct a Click MultiCommand group such that I'm able to supply a combination of options, subcommands and subcommand options, both from the commandline and from a configuration file.
There are a few examples of driving Click from a config file already:
https://github.com/click-contrib/click-configfile
https://github.com/psf/black/blob/37861b4ce264f16754f4459d19522a05844daf9f/src/black/__init__.py#L99
Python Click - Supply arguments and options from a configuration file
These make sense to me and I'm able to implement reading options from a JSON file as per the example below (based on how Black is doing this). The common point amongst these example implementations is that they work by overriding the default option values via an eager callback function (so these defaults are set before the rest of the commandline is parsed). Thus if both a config file and a commandline option are supplied then the commandline option will be used. This is fine, but to actually have Click invoke any subcommands they must be supplied from the commandline (but may have purely config driven options).
I would like to also specify which subcommands to run from the passed in config file (as well as options) but cannot determine the best way to do this. I have tried modifying the Click context args
property at the same point as I modify the default_map
property, but this is simply overwritten by Clicks default behaviour when it comes to parse the commandline arguments (where no subcommands were passed) and args
returns to an empty list.
# commandline.py
import click
import json
def read_config(ctx, param, value):
if value is None:
return
with open(value) as fh:
config = json.load(fh)
new_defaults = {}
if ctx.default_map:
new_defaults.update(ctx.default_map)
config = {key: val if isinstance(val, (list, dict)) else str(val) for key, val in config.items()}
new_defaults.update(config)
ctx.default_map = new_defaults
return value
@click.option(
"--opt_a",
)
@click.option(
"--config",
is_eager=True,
callback=read_config,
)
@click.group(
chain=True,
no_args_is_help=True,
invoke_without_command=True,
)
@click.pass_context
def cli(ctx, opt_a, config):
click.echo(f"opt_a was '{opt_a}'")
click.echo(f"config was '{config}'")
@click.option(
"--sub_opt_a",
)
@cli.command("sub_cmd_a")
def sub_cmd_a(sub_opt_a):
click.echo(f"sub_cmd_a called with '{sub_opt_a}'")
@click.option(
"--sub_opt_b",
)
@cli.command("sub_cmd_b")
def sub_cmd_b(sub_opt_b):
click.echo(f"sub_cmd_b called with '{sub_opt_b}'")
if __name__ == "__main__":
cli()
I'm aiming for these calls to be equivalent:
Fully command line specified
python commandline.py --opt_a "Test Opt A" sub_cmd_a --sub_opt_a "Test SubOpt A"
Fully configuration file specified
python commandline.py --config "config_file.json"
Where config_file.json is
{
"opt_a": "Test Opt A",
"sub_cmd_a": {
"sub_opt_a": "Test SubOpt A"
}
}
Appreciate any pointers on being able to inject subcommand arguments into Click. Thanks.
Upvotes: 1
Views: 825