I have a script with subcommands that both require a username/password. I abstracted this to the parent. Here's a short example demonstrating the issue:
import click
@click.group()
@click.option('--username', default='admin', prompt=True)
@click.password_option()
def cli(username, password):
"""This is my parent description"""
pass
@cli.command()
@click.option('--reference', help="I should be able to see this without entering a username and password.")
def subcommand(reference):
"""This is my subcommand"""
pass
If you access the main help:
my_prog --help
It displays the help page without any prompting for username or password, as expected. However, if you try to access the help page for the subcommand:
my_prog subcommand --help
It prompts for username and password before displaying the help page of the subcommand. This makes sense, but it still isn't expected behavior by the end user. As my sample program says, I should be able to see help without entering a username and password.
What's the correct way to model my desired behavior? Do I have to move and duplicate the username and password prompt options into each of my sub commands?
@philipsd6 Same issue, I was going to approach this problem by removing the prompt=True on the option and checking for the required options but realized that sub-commands would have to do the same. Is there no directive for Ignoring prompts on certain options
+1 for fixing this.
Have you tried using an eager option?
http://click.pocoo.org/3/options/#callbacks-and-eager-options
@ryanneufeld This is a nice suggestion but how do you define that for the default provided --help option ?
http://click.pocoo.org/3/advanced/#callback-evaluation-order
@warsamebashir you can override the help command with your own.
@ryanneufeld do you have an example of how we can do that. I couldn't find anything in the docs. Is it just the traditional @click.option decorator with the is_flag=True set ?
This is a tough one to fix and I am not sure if it will be entirely possible without causing more issues elsewhere. :(
+1 for fixing this as well.
In a similar situation, instead of trying to pull the common behavior up to the group level, I defined a decorator that implemented the behavior and used that on the individual commands. For example, something like (I haven't actually checked that this code works...):
from functools import wraps
def needs_username_and_password(f):
@wraps(f)
@click.option('--username', default='admin', prompt=True)
@click.password_option()
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
@click.group()
def cli():
"""This is my parent description"""
pass
@cli.command()
@click.option('--reference', help="I should be able to see this without entering a username and password.")
@needs_username_and_password
def subcommand(username, password, reference):
"""This is my subcommand"""
pass
Hey @vokal-isaac, thanks for your solution! It was throwing an error as-is for me but I was able to fix.
$ python -V; ./test.py subcommand --help
Python 2.7.15
Traceback (most recent call last):
File "./test.py", line 25, in <module>
@needs_username_and_password
File "/usr/local/lib/python2.7/site-packages/click/decorators.py", line 170, in decorator
_param_memo(f, OptionClass(param_decls, **attrs))
File "/usr/local/lib/python2.7/site-packages/click/decorators.py", line 135, in _param_memo
f.__click_params__ = []
AttributeError: 'NoneType' object has no attribute '__click_params__'
Adding return wrapper to needs_username_and_password resolved issue. Complete example:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import click
from functools import wraps
def needs_username_and_password(f):
@wraps(f)
@click.option('--username', default='admin', prompt=True)
@click.password_option()
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper # **** Added return ****
@click.group()
def cli():
"""This is my parent description"""
pass
@cli.command()
@click.option('--reference', help="I should be able to see this without entering a username and password.")
@needs_username_and_password
def subcommand(username, password, reference):
"""This is my subcommand"""
pass
if __name__ == '__main__':
cli()
@druchoo Ah, yeah, looking at it now, it was definitely missing that return wrapper. (As I'd said, "I haven't actually checked that this code works..." 馃槈)
I like the solution above a lot, but I ran into issues trying to "generalize" this by using multiple decorators (I've posted on SO as well). The issue with the code below is that only the decorator that is "applied" to the bare function will properly get the @click.options in its argument list.
Is there a way to properly propagate wrap the functions as to propagate the __click_params__ that @click.option adds to a function?
from functools import wraps
import click
def init_session(f):
@wraps(f)
@click.option('--argument-B')
def wrapper(*args, **kwargs):
# do something with argument-B, and add session to list of arguments.
del kwargs['argument-B']
kwargs['session'] = session_created_above
return f(*args,**kwargs)
return wrapper
def parse_config_file(f):
@wraps(f)
@click.option('--argument-C')
def wrapper(*args, **kwargs):
# do something with argument-C, and add config to list of arguments.
del kwargs['argument-C']
kwargs['config'] = config_parsed_above
return f(*args,**kwargs)
return wrapper
@click.group()
def main():
pass
@main.command()
@click.option('--argument-a')
@parse_config_file
@init_session
def do_something_in_session(argument_a, config, session):
# code
return
The issue with the decorator solution is that all of your group options get moved to command options, meaning the "global stuff" that all your commands need now has to be specified after the command, i.e this:
cli --some-global-option command
Becomes:
cli command --some-global-option
I don't know, but for some reason this irks me, I guess because of the mental model of group options applying to all the commands through context it makes intuitive sense to me to specify them before the command.
I ran into this issue with a required group option, I implemented a solution based on the answer of @ryanneufeld. I don't know if it works with every corner cases, but it shows the command help when the help flag is given or no arguments given. And returns an error when trying to use the command without help. I also tested it with the first example in this issue and it seems to work.
hope it can help someone
import sys
import click
CONTEXT = {"help_option_names": ["-h", "--help"]}
def print_cmd_help(ctx, param, value):
group = ctx.command
# parse the cmdline and get the command and its arguments
parser = group.make_parser(ctx)
global_opts, args, _ = parser.parse_args(args=sys.argv[1:])
if global_opts.get("help") is True or not args:
# global help wanted
click.echo(ctx.get_help())
ctx.exit()
# create the command object manually and parse its arguments
name, cmd, args = group.resolve_command(ctx, args)
help_names = cmd.get_help_option_names(ctx)
if set(args) & help_names or not args:
# return command help when a user wants it or when he does not provide
# arguments
# taken from https://github.com/pallets/click/blob/7.1.1/src/click/core.py#L597
cmd_ctx = click.Context(cmd, info_name=cmd.name, parent=ctx)
click.echo(cmd_ctx.get_help())
cmd_ctx.exit()
@click.group(context_settings=CONTEXT)
@click.option("--username", default="admin", prompt=True)
@click.password_option()
@click.option(
"-h",
"--help",
is_flag=True,
callback=print_cmd_help,
expose_value=False,
is_eager=True,
)
def cli(username, password):
...
@cli.command(help="this is the help of test")
@click.argument("name")
def test(name):
click.echo(name)
cli()
I don't think this is solvable within Click in a general way.
Click parses and process all args and callbacks for a command before moving on to a subcommand. The subcommand can be resolved dynamically based on the current command's context, so it's not possible to guarantee what commands and params will be processed before actually running the pipeline. Additionally, the help option isn't guaranteed to be --help, and --help could also show up as the value of another option, so it's not possible to guarantee that --help showing up in the raw args means the help callback will be executed.
There are a few different solutions a project can implement:
prompt_required argument, that changes the behavior of prompts so they're only shown if the option is required or if the flag without a value is given. This applies to prompts, but it doesn't apply to missing required parameters.prompt=True in the parameters, use a helper function before the values are actually used that will call click.prompt() if they weren't provided.--help param can be customized to figure out the subcommand and show its help.help command (like git help <command name>) is fairly straightforward to implement using group.get_command(ctx, name) and command.get_help(ctx).
Most helpful comment
This is a tough one to fix and I am not sure if it will be entirely possible without causing more issues elsewhere. :(