This can be done by building a custom class derived from click.Option, and in that class over riding the click.Option.handle_parse_result() method like:
Custom Class:
import click
class NotRequiredIf(click.Option):
def __init__(self, *args, **kwargs):
self.not_required_if = kwargs.pop('not_required_if')
assert self.not_required_if, "'not_required_if' parameter required"
kwargs['help'] = (kwargs.get('help', '') +
' NOTE: This argument is mutually exclusive with %s' %
self.not_required_if
).strip()
super(NotRequiredIf, self).__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
we_are_present = self.name in opts
other_present = self.not_required_if in opts
if other_present:
if we_are_present:
raise click.UsageError(
"Illegal usage: `%s` is mutually exclusive with `%s`" % (
self.name, self.not_required_if))
else:
self.prompt = None
return super(NotRequiredIf, self).handle_parse_result(
ctx, opts, args)
Using Custom Class:
To use the custom class, pass the cls parameter to click.option decorator like:
@click.option('--username', prompt=True, cls=NotRequiredIf,
not_required_if='authentication_token')
How does this work?
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 disable the need to user/password if authentication-token token is present, and complain if both user/password are authentication-token are present.
Note: This answer was inspired by this answer
Test Code:
@click.command()
@click.option('--authentication-token')
@click.option('--username', prompt=True, cls=NotRequiredIf,
not_required_if='authentication_token')
@click.option('--password', prompt=True, hide_input=True, cls=NotRequiredIf,
not_required_if='authentication_token')
def login(authentication_token, username, password):
click.echo('t:%s u:%s p:%s' % (
authentication_token, username, password))
if __name__ == '__main__':
login('--username name --password pword'.split())
login('--help'.split())
login(''.split())
login('--username name'.split())
login('--authentication-token token'.split())
Results:
from login('--username name --password pword'.split()):
t:None u:name p:pword
from login('--help'.split()):
Usage: test.py [OPTIONS]
Options:
--authentication-token TEXT
--username TEXT NOTE: This argument is mutually exclusive with
authentication_token
--password TEXT NOTE: This argument is mutually exclusive with
authentication_token
--help Show this message and exit.
Answer from Stephen Rauch on Stack OverflowThis can be done by building a custom class derived from click.Option, and in that class over riding the click.Option.handle_parse_result() method like:
Custom Class:
import click
class NotRequiredIf(click.Option):
def __init__(self, *args, **kwargs):
self.not_required_if = kwargs.pop('not_required_if')
assert self.not_required_if, "'not_required_if' parameter required"
kwargs['help'] = (kwargs.get('help', '') +
' NOTE: This argument is mutually exclusive with %s' %
self.not_required_if
).strip()
super(NotRequiredIf, self).__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
we_are_present = self.name in opts
other_present = self.not_required_if in opts
if other_present:
if we_are_present:
raise click.UsageError(
"Illegal usage: `%s` is mutually exclusive with `%s`" % (
self.name, self.not_required_if))
else:
self.prompt = None
return super(NotRequiredIf, self).handle_parse_result(
ctx, opts, args)
Using Custom Class:
To use the custom class, pass the cls parameter to click.option decorator like:
@click.option('--username', prompt=True, cls=NotRequiredIf,
not_required_if='authentication_token')
How does this work?
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 disable the need to user/password if authentication-token token is present, and complain if both user/password are authentication-token are present.
Note: This answer was inspired by this answer
Test Code:
@click.command()
@click.option('--authentication-token')
@click.option('--username', prompt=True, cls=NotRequiredIf,
not_required_if='authentication_token')
@click.option('--password', prompt=True, hide_input=True, cls=NotRequiredIf,
not_required_if='authentication_token')
def login(authentication_token, username, password):
click.echo('t:%s u:%s p:%s' % (
authentication_token, username, password))
if __name__ == '__main__':
login('--username name --password pword'.split())
login('--help'.split())
login(''.split())
login('--username name'.split())
login('--authentication-token token'.split())
Results:
from login('--username name --password pword'.split()):
t:None u:name p:pword
from login('--help'.split()):
Usage: test.py [OPTIONS]
Options:
--authentication-token TEXT
--username TEXT NOTE: This argument is mutually exclusive with
authentication_token
--password TEXT NOTE: This argument is mutually exclusive with
authentication_token
--help Show this message and exit.
Slightly improved Stephen Rauch's answer to have multiple mutex parameters.
import click
class Mutex(click.Option):
def __init__(self, *args, **kwargs):
self.not_required_if:list = kwargs.pop("not_required_if")
assert self.not_required_if, "'not_required_if' parameter required"
kwargs["help"] = (kwargs.get("help", "") + "Option is mutually exclusive with " + ", ".join(self.not_required_if) + ".").strip()
super(Mutex, self).__init__(*args, **kwargs)
def handle_parse_result(self, ctx, opts, args):
current_opt:bool = self.name in opts
for mutex_opt in self.not_required_if:
if mutex_opt in opts:
if current_opt:
raise click.UsageError("Illegal usage: '" + str(self.name) + "' is mutually exclusive with " + str(mutex_opt) + ".")
else:
self.prompt = None
return super(Mutex, self).handle_parse_result(ctx, opts, args)
use like this:
@click.group()
@click.option("--username", prompt=True, cls=Mutex, not_required_if=["token"])
@click.option("--password", prompt=True, hide_input=True, cls=Mutex, not_required_if=["token"])
@click.option("--token", cls=Mutex, not_required_if=["username","password"])
def login(ctx=None, username:str=None, password:str=None, token:str=None) -> None:
print("...do what you like with the params you got...")
Documentation: mention required=True for options
python - Require an option only if a choice is made when using click - Stack Overflow
command line interface - how to specify a required option out of 3 in python `click` - Stack Overflow
python - Dependent options on another option - Stack Overflow
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
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.
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.
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")
One way that I have managed to achieve this behaviour was by actually using arguments as below. I'll post this as a workaround, while I try to see if it could be done with an option, and I'll update my post accordingly
@click.command(context_settings={"ignore_unknown_options": True})
@click.argument("options", nargs=-1)
def myscript(options):
option = False
if options is ():
option = False
if '-o' in options or '--option' in options:
option = True
if len(options) > 1:
option = options[1]
print(option)
Later Edit Using an option, I have managed to achieve this by adding an argument to the command definition.
@click.command()
@click.option('-o', '--option', is_flag=True, default=False, is_eager=True)
@click.argument('value', nargs=-1)
def myscript(option, value):
if option and value != ():
option = value[0]
print(option)
The nargs can be removed if you only expect at most one argument to follow, and can be treated as not required.
@click.command()
@click.option('-o', '--option', is_flag=True, default=False, is_eager=True)
@click.argument('value', required=False)
def myscript(option, value=None):
if option and value is not None:
option = value
print(option)
It might also be possible by putting together a context generator and storing some state, but that seems the least desirable solution, since you would be relying on the context storing your state.
FYI, if anybody bumps into this problem in 2024, it seems that click has made something specific for this use case, see: https://click.palletsprojects.com/en/8.1.x/options/#optional-value
Setting is_flag=False, flag_value=value tells Click that the option can still be passed a value, but if only the flag is given the flag_value is used.
@click.command()
@click.option("--name", is_flag=False, flag_value="Flag", default="Default")
def hello(name):
click.echo(f"Hello, {name}!")
>> hello
Hello, Default!
>> hello --name Value
Hello, Value!
>> hello --name
Hello, Flag!
This can be done by over riding the click.Group argument parser like:
Custom Class:
class MyGroup(click.Group):
def parse_args(self, ctx, args):
if args[0] in self.commands:
if len(args) == 1 or args[1] not in self.commands:
args.insert(0, '')
super(MyGroup, self).parse_args(ctx, args)
Using Custom Class:
Then to use the custom group, pass it as the cls argument to the group decorator like:
@click.group(cls=MyGroup)
@click.argument('hostname', required=False)
@click.pass_context
def cli(ctx, hostname=None):
....
How?
This works because click is a well designed OO framework. The @click.group() decorator usually instantiates a click.Group object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Group in our own class and over ride desired methods.
In this case we over ride click.Group.parse_args() and if the first parameter matches a command and the second parameter does not, then we insert an empty string as the first parameter. This puts everything back where the parser expects it to be.
I was trying to figure this out..
How I did it was two ways.
- Default the value to something like a empty string. I prefer to give it a value for what I was doing since I had code reading that variable.
@click.argument( "myarg", default="", )
- Other way was to say it is not required.
@click.argument( "myarg", required=False, )