13

This question is about the click package: I want to setup my command so that some optional options are dependent on a specific option value and are required based on its value.

Required options:

  1. input (input file)
  2. doe (integer , represents algo name)

Sub options: if doe is

  1. equal to 1 then option generator_string should become required=True
  2. equal to 2 then option number_of_sample_pointsshould become required=True
  3. equal to 3 then option number_of_center_pointsshould become required=True

Valid Examples:

  1. --input ./input.txt --doe 1 --generator_string 1234
  2. --input ./input.txt --doe 2 --number_of_sample_points 3
  3. --input ./input.txt --doe 3 --number_of_center_points 2

CODE:

import click
def check_output(ctx, param, value):
 if value == 1:
 if not ctx.params['generator_string']:
 setOptionAsRequired(ctx, 'generator_string')
 return value
def setOptionAsRequired(ctx, name):
 for p in ctx.command.params:
 if isinstance(p, click.Option) and p.name == name:
 p.required = True
@click.option('--input', required=True, type=click.Path(exists=True) )
@click.option('--doe', required=True, type=int, callback=check_output )
@click.option('--generator_string', required=False, type=str, is_eager=True)
@click.option('--number_of_sample_points', required=False, type=int, is_eager=True)
@click.option('--number_of_center_points', required=False, type=int, is_eager=True)
@click.command(context_settings=dict(max_content_width=800))
def main(input, doe, generator_string, number_of_sample_points, number_of_center_points):
 click.echo('is valid command')
if __name__ == '__main__':
 main()
codeforester
43.8k21 gold badges123 silver badges159 bronze badges
asked Apr 9, 2019 at 1:52
2
  • You can make the 3 possible values of --doe as subcommands. Each subcommand can have completely different options. Commented Apr 9, 2019 at 2:37
  • Yes but i dont have commands like sync , i just have options or arguments , see my examples , also algos have very long name that`s why i am using numbers. any ideas ? Commented Apr 9, 2019 at 3:10

2 Answers 2

18

I would suggest doing that with a custom click.Command class like:

Custom Class:

def command_required_option_from_option(require_name, require_map):
 class CommandOptionRequiredClass(click.Command):
 def invoke(self, ctx):
 require = ctx.params[require_name]
 if require not in require_map:
 raise click.ClickException(
 "Unexpected value for --'{}': {}".format(
 require_name, require))
 if ctx.params[require_map[require].lower()] is None:
 raise click.ClickException(
 "With {}={} must specify option --{}".format(
 require_name, require, require_map[require]))
 super(CommandOptionRequiredClass, self).invoke(ctx)
 return CommandOptionRequiredClass

Using the Custom Class

required_options = {
 1: 'generator_string',
 2: 'number_of_sample_points',
 3: 'number_of_center_points',
}
@click.command(cls=command_required_option_from_option('doe', required_options))
...

How does this work?

This works because click is a well designed OO framework. The @click.command() decorator usually instantiates a click.Command object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Command in our own class and over ride desired methods.

In this case, we override click.Command.invoke() and then validate that the required option has been set before running the command

Test Code:

import click
required_options = {
 1: 'generator_string',
 2: 'number_of_sample_points',
 3: 'number_of_center_points',
}
@click.command(context_settings=dict(max_content_width=800),
 cls=command_required_option_from_option('doe', required_options))
@click.option('--input', required=True,
 type=click.Path(exists=True))
@click.option('--doe', required=True, type=int)
@click.option('--generator_string', required=False, type=str, is_eager=True)
@click.option('--number_of_sample_points', required=False, type=int,
 is_eager=True)
@click.option('--number_of_center_points', required=False, type=int,
 is_eager=True)
def main(input, doe, generator_string, number_of_sample_points,
 number_of_center_points):
 click.echo('input: {}'.format(input))
 click.echo('doe: {}'.format(doe))
 click.echo('generator_string: {}'.format(generator_string))
 click.echo('Num of sample_points: {}'.format(number_of_sample_points))
 click.echo('Num of center_points: {}'.format(number_of_center_points))
if __name__ == "__main__":
 commands = (
 '--input ./input.txt --doe 0',
 '--input ./input.txt --doe 1',
 '--input ./input.txt --doe 2',
 '--input ./input.txt --doe 3',
 '--input ./input.txt --doe 1 --generator_string 1234',
 '--input ./input.txt --doe 2 --number_of_sample_points 3',
 '--input ./input.txt --doe 3 --number_of_center_points 2',
 '',
 '--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)
 main(cmd.split())
 except BaseException as exc:
 if str(exc) != '0' and \
 not isinstance(exc, (click.ClickException, SystemExit)):
 raise

Results:

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)]
-----------
> --input ./input.txt --doe 0
Error: Unexpected value for --'doe': 0
-----------
> --input ./input.txt --doe 1
Error: With doe=1 must specify option --generator_string
-----------
> --input ./input.txt --doe 2
Error: With doe=2 must specify option --number_of_sample_points
-----------
> --input ./input.txt --doe 3
Error: With doe=3 must specify option --number_of_center_points
-----------
> --input ./input.txt --doe 1 --generator_string 1234
input: ./input.txt
doe: 1
generator_string: 1234
Num of sample_points: None
Num of center_points: None
-----------
> --input ./input.txt --doe 2 --number_of_sample_points 3
input: ./input.txt
doe: 2
generator_string: None
Num of sample_points: 3
Num of center_points: None
-----------
> --input ./input.txt --doe 3 --number_of_center_points 2
input: ./input.txt
doe: 3
generator_string: None
Num of sample_points: None
Num of center_points: 2
-----------
> 
Usage: test.py [OPTIONS]
Error: Missing option "--input".
-----------
> --help
Usage: test.py [OPTIONS]
Options:
 --input PATH [required]
 --doe INTEGER [required]
 --generator_string TEXT
 --number_of_sample_points INTEGER
 --number_of_center_points INTEGER
 --help Show this message and exit.
answered Apr 9, 2019 at 3:51
Sign up to request clarification or add additional context in comments.

5 Comments

Not relevant question but click does not support camelCase ? | If i write @click.option('--generatorString') it does not work
also I think i dont need , is_eager=True for sub required options .. right ??
Click normalizes the names. I made a minor edit to allow camel case. You will still need to have main() take a flattened case, but to the user it will be camelCase.
asked a formatting question here , stackoverflow.com/questions/55585564/…
is it possible to do the same with prompts?
1

If you're OK with an additional dependency there's click-constrained-option. Seems very comprehensive. I've used it as follows:

@click.option("--location", "-l", type=click.STRING, help="Get location (Town/City)", required=False)
@click.option("--dms", is_flag=True, default=False, cls=ConstrainedOption,allowed_if="location", help="Display Degrees, Minutes, Seconds")
answered Mar 29, 2025 at 9:50

1 Comment

Unfortunately click-constrained-option is now archived and doesn't work with click 8

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.