Reputation: 13051
When using click
I know how to define a multiple choice option. I also know how to set an option as a required one. But, how can I indicate that an option B
is required only if the value of option A
is foo
?
Here's an example:
import click
@click.command()
@click.option('--output',
type=click.Choice(['stdout', 'file']), default='stdout')
@click.option('--filename', type=click.STRING)
def main(output, filename):
print("output: " + output)
if output == 'file':
if filename is None:
print("filename must be provided!")
else:
print("filename: " + str(filename))
if __name__ == "__main__":
main()
If the output
option is stdout
, then filename
is not needed. However, if the user chooses output
to be file
, then the other option filename
must be provided. Is this pattern supported by click?
At the beginning of the function I can add something like:
if output == 'file' and filename is None:
raise ValueError('When output is "file", a filename must be provided')
But I am interested whether there's a nicer/cleaner solution.
Upvotes: 11
Views: 4128
Reputation: 13051
I extended the answer by Stephen, and made it more generic:
class OptionRequiredIf(click.Option):
"""
Option is required if the context has `option` set to `value`
"""
def __init__(self, *a, **k):
try:
option = k.pop('option')
value = k.pop('value')
except KeyError:
raise(KeyError("OptionRequiredIf needs the option and value "
"keywords arguments"))
click.Option.__init__(self, *a, **k)
self._option = option
self._value = value
def process_value(self, ctx, value):
value = super(OptionRequiredIf, self).process_value(ctx, value)
if value is None and ctx.params[self._option] == self._value:
msg = 'Required if --{}={}'.format(self._option, self._value)
raise click.MissingParameter(ctx=ctx, param=self, message=msg)
return value
Usage example:
@click.option('--email', type=click.STRING,
help='Settings for sending emails.',
option='output', value='email', cls=OptionRequiredIf)
I was inspired by this answer
Upvotes: 2
Reputation: 311606
You can do the same thing with a custom validation callback:
import click
def required_with_output(ctx, param, value):
if ctx.params.get("output") != "stdout" and value is None:
raise click.BadParameter("--output requires --filename")
return value
@click.command()
@click.option(
"--output",
type=click.Choice(["stdout", "file"]),
default="stdout",
)
@click.option("--filename", callback=required_with_output)
def main(output, filename):
print("output: " + output)
if output == "file":
if filename is None:
print("filename must be provided!")
else:
print("filename: " + str(filename))
if __name__ == "__main__":
main()
I think this is a little simpler.
Upvotes: 3
Reputation: 49794
In the particular case of this example, I think an easier method would be to get rid of --output
, and simply assume stdout
if --filename
is not specified and if --filename
is specified, then use it instead of stdout
.
But assuming this is a contrived example, you can inherit from click.Option
to allow hooking into the click processing:
class OptionRequiredIf(click.Option):
def full_process_value(self, ctx, value):
value = super(OptionRequiredIf, self).full_process_value(ctx, value)
if value is None and ctx.params['output'] == 'file':
msg = 'Required if --output=file'
raise click.MissingParameter(ctx=ctx, param=self, message=msg)
return value
To use the custom class, pass it as the cls argument to the option decorator like:
@click.option('--filename', type=click.STRING, cls=OptionRequiredIf)
import click
@click.command()
@click.option('--output',
type=click.Choice(['stdout', 'file']), default='stdout')
@click.option('--filename', type=click.STRING, cls=OptionRequiredIf)
def main(output, filename):
print("output: " + output)
if output == 'file':
if filename is None:
print("filename must be provided!")
else:
print("filename: " + str(filename))
main('--output=file'.split())
Usage: test.py [OPTIONS]
Error: Missing option "--filename". Required if --output=file
Upvotes: 7