I want to use several options together, or not at all, as the title says, but my methods seem relatively ugly, and I was wondering if there was a cleaner way to implement this. I have, in addition, looked at this, about how it might be done in argparse
, but I would like to implement it in click
if possible (I am trying to avoid using nargs=[...]
).
So far, this is what I have:
@click.group(invoke_without_command=True, no_args_is_help=True)
@click.option(
"-d",
"--group-dir",
type=click.Path(),
default="default",
help='the directory to find the TOML file from which to run multiple jobs at the same time; defaults to the configuration directory of melmetal: "~/.melmetal" on Unix systems, and "C:\\Users\\user\\.melmetal" on Windows',
)
@click.option("-f", "--group-file", help="the TOML file name")
@click.option(
"-n", "--group-name", help="name of the group of jobs"
)
@click.option(
"--no-debug",
is_flag=True,
type=bool,
help="prevent logging from being output to the terminal",
)
@click.pass_context
@logger.catch
def main(ctx, group_dir, group_file, group_name, no_debug):
options = [group_file, group_name]
group_dir = False if not any(options) else group_dir
options.append(group_dir)
if not any(options):
pass
elif not all(options):
logger.error(
colorize("red", "Sorry; you must use all options at once.")
)
exit(1)
else:
[...]
And a second example:
if any(createStuff):
if not all(createStuff):
le(
colorize("red", 'Sorry; you must use both the "--config-dir" and "--config-file" options at once.')
)
exit(1)
elif any(filtered):
if len(filtered) is not len(drbc):
le(
colorize("red", 'Sorry; you must use all of "--device", "--repo-name", "--backup-type", and "--config-dir" at once.')
)
exit(1)
else:
ctx = click.get_current_context()
click.echo(ctx.get_help())
exit(0)
How do I get the help text to show when no sub-commands are given? As I understand it, this is supposed to happen automatically, but for my code it automatically goes to the main function. An example of my workaround is in the second example, i.e. under the else
statement.
You can enforce using all options in a group by building a custom class derived from click.Option
, and in that class over riding the click.Option.handle_parse_result()
method like:
import click
class GroupedOptions(click.Option):
def __init__(self, *args, **kwargs):
self.opt_group = kwargs.pop('opt_group')
assert self.opt_group, "'opt_group' parameter required"
super(GroupedOptions, self).__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
if self.name in opts:
opts_in_group = [param.name for param in ctx.command.params
if isinstance(param, GroupedOptions) and
param.opt_group == self.opt_group]
missing_specified = tuple(name for name in opts_in_group
if name not in opts)
if missing_specified:
raise click.UsageError(
"Illegal usage: When using option '{}' must also use "
"all of options {}".format(self.name, missing_specified)
)
return super(GroupedOptions, self).handle_parse_result(
ctx, opts, args)
To use the custom class, pass the cls
parameter to click.option
decorator like:
@click.option('--opt1', cls=GroupedOptions, opt_group=1)
In addition give an option group number with the opt_group
parameter.
This works because click is a well designed OO framework. The @click.option()
decorator usually instantiates a click.Option
object but allows this behavior to be overridden with the cls
parameter. So it is a relatively easy matter to inherit from click.Option
in our own class and over ride the desired methods.
In this case we over ride click.Option.handle_parse_result()
and check that other options in our group were specified.
Note: This answer was inspired by this answer
@click.command()
@click.option('--opt1', cls=GroupedOptions, opt_group=1)
@click.option('--opt2', cls=GroupedOptions, opt_group=1)
@click.option('--opt3', cls=GroupedOptions, opt_group=1)
@click.option('--opt4', cls=GroupedOptions, opt_group=2)
@click.option('--opt5', cls=GroupedOptions, opt_group=2)
def cli(**kwargs):
for arg, value in kwargs.items():
click.echo("{}: {}".format(arg, value))
if __name__ == "__main__":
commands = (
'--opt1=x',
'--opt4=a',
'--opt4=a --opt5=b',
'--opt1=x --opt2=y --opt3=z --opt4=a --opt5=b',
'--help',
'',
)
import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
try:
time.sleep(0.1)
print('-----------')
print('> ' + cmd)
time.sleep(0.1)
cli(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --opt1=x
Error: Illegal usage: When using option 'opt1' must also use all of options ('opt2', 'opt3')
-----------
> --opt4=a
Error: Illegal usage: When using option 'opt4' must also use all of options ('opt5',)
-----------
> --opt4=a --opt5=b
opt4: a
opt5: b
opt1: None
opt2: None
opt3: None
-----------
> --opt1=x --opt2=y --opt3=z --opt4=a --opt5=b
opt1: x
opt2: y
opt3: z
opt4: a
opt5: b
-----------
> --help
Usage: test.py [OPTIONS]
Options:
--opt1 TEXT
--opt2 TEXT
--opt3 TEXT
--opt4 TEXT
--opt5 TEXT
--help Show this message and exit.
-----------
>
opt1: None
opt2: None
opt3: None
opt4: None
opt5: None