11

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.

i alarmed alien
9,5403 gold badges30 silver badges40 bronze badges
asked Sep 27, 2017 at 6:32

3 Answers 3

7

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:

Custom Class:

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

Using Custom Class:

To use the custom class, pass it as the cls argument to the option decorator like:

@click.option('--filename', type=click.STRING, cls=OptionRequiredIf)

Test Code:

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())

Results:

Usage: test.py [OPTIONS]
Error: Missing option "--filename". Required if --output=file
Dror
13.2k24 gold badges101 silver badges172 bronze badges
answered Sep 27, 2017 at 15:22
Sign up to request clarification or add additional context in comments.

3 Comments

This seems to be very elegant solution. I tried to modify it a little and provide two more parameters to the class OptionRequiredIf. One would be depends_on_opt which in this example would be output and the other depends_on_val which would hold file. I failed...
I'm afraid this solution doesn't work... When providing the filename it is not populating the corresponding variable. I posted a complete example on gist
@dror, sorry I missed the return value, I was getting late for work, so I didn't test as well as I should have. Glad it worked out and thanks for the edit...
3

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.

answered Jan 13, 2023 at 16:26

Comments

2

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

answered Oct 10, 2017 at 8:56

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.