Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Python Click how do I see --help for Subcommands whose parents have required arguments?

My program uses Click for command line processing. It has a main command that takes a required argument. This command has subcommands that take optional arguments. Different subcommands take different options, but they all require the same argument from their parent. I'd like to have the command line look like this:

python myprogram.py argument-value subcommand1 --option-1=value

I can write this using Click like so

import click

@click.group()
@click.argument("argument")
@click.pass_context
def main(context, argument):
    """ARGUMENT is required for both subcommands"""
    context.obj = {"argument": argument}


@click.command()
@click.option("--option-1", help="option for subcommand 1")
@click.pass_context
def subcommand1(context, option_1):
    print("subcommand 1: %s %s" % (context.obj["argument"], option_1))


@click.command()
@click.option("--option-2", help="option for subcommand 2")
@click.pass_context
def subcommand2(context, option_2):
    print("subcommand 2: %s %s" % (context.obj["argument"], option_2))


main.add_command(subcommand1)
main.add_command(subcommand2)

if __name__ == "__main__":
    main()

The top-level help message is what I want.

python myprogram.py --help
Usage: myprogram.py [OPTIONS] ARGUMENT COMMAND [ARGS]...

  ARGUMENT is required for both subcommands

Options:
  --help  Show this message and exit.

Commands:
  subcommand1
  subcommand2

I can get help for a subcommand if I pass in the required argument.

python myprogram.py dummy-argument subcommand1 --help
Usage: myprogram.py subcommand1 [OPTIONS]

Options:
  --option-1 TEXT  option for subcommand 1
  --help           Show this message and exit.

However, I'd like to get the subcommand help without requiring the user to pass in a dummy argument. I want to be able to run python myprogram.py subcommand1 --help and see the same output as above, but instead I just get the help text for the top level.

python myprogram.py subcommand1 --help
Usage: myprogram.py [OPTIONS] ARGUMENT COMMAND [ARGS]...

  ARGUMENT is required for both subcommands

Options:
  --help  Show this message and exit.

Commands:
  subcommand1
  subcommand2

Is there a way to get the behavior I want? I realize that Click puts a premium on having each of its commands be self-contained, but this seems like a common scenario.

like image 774
W.P. McNeill Avatar asked Nov 22 '17 14:11

W.P. McNeill


2 Answers

There is an inherent ambiguity in your requirements, in that the subcommand name could possibly be the same as a valid value for the common argument.

So, some way to disambiguate is required. I present one possible solution below.

When finding a value of the argument which matches a subcommand name, the proposed solution will search for the presence of --help. If found it then assumes that help is being requested for the subcommand, and will populate the dummy-argument automatically.

Custom Class:

import click

class PerCommandArgWantSubCmdHelp(click.Argument):

    def handle_parse_result(self, ctx, opts, args):
        # check to see if there is a --help on the command line
        if any(arg in ctx.help_option_names for arg in args):

            # if asking for help see if we are a subcommand name
            for arg in opts.values():
                if arg in ctx.command.commands:

                    # this matches a sub command name, and --help is
                    # present, let's assume the user wants help for the
                    # subcommand
                    args = [arg] + args

        return super(PerCommandArgWantSubCmdHelp, self).handle_parse_result(
            ctx, opts, args)

Using Custom Class:

To use the custom class, pass the cls parameter to @click.argument() decorator like:

@click.argument("argument", cls=PerCommandArgWantSubCmdHelp)

How does this work?

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

In this case we over ride click.Argument.handle_parse_result() and look for the pattern of a subcommand name followed by --help. When found, we then doctor the arguments list to get the pattern click needs to parse this the way want to show the subcommand help.

Test Code:

@click.group()
@click.argument("argument", cls=PerCommandArgWantSubCmdHelp)
@click.pass_context
def main(context, argument):
    """ARGUMENT is required for both subcommands"""
    context.obj = {"argument": argument}


@click.command()
@click.option("--option-1", help="option for subcommand 1")
@click.pass_context
def subcommand1(context, option_1):
    print("subcommand 1: %s %s" % (context.obj["argument"], option_1))


@click.command()
@click.option("--option-2", help="option for subcommand 2")
@click.pass_context
def subcommand2(context, option_2):
    print("subcommand 2: %s %s" % (context.obj["argument"], option_2))


main.add_command(subcommand1)
main.add_command(subcommand2)

if __name__ == "__main__":
    commands = (
        'subcommand1 --help',
        'subcommand2 --help',
        'dummy-argument subcommand1 --help',
    )

    for cmd in commands:
        try:
            print('-----------')
            print('> ' + cmd)
            main(cmd.split())
        except:
            pass

Test Results:

-----------
> subcommand1 --help
Backend TkAgg is interactive backend. Turning interactive mode on.
Usage: test.py subcommand1 [OPTIONS]

Options:
  --option-1 TEXT  option for subcommand 1
  --help           Show this message and exit.
-----------
> subcommand2 --help
Usage: test.py subcommand2 [OPTIONS]

Options:
  --option-2 TEXT  option for subcommand 2
  --help           Show this message and exit.
-----------
> dummy-argument subcommand1 --help
Usage: test.py subcommand1 [OPTIONS]

Options:
  --option-1 TEXT  option for subcommand 1
  --help           Show this message and exit.                
like image 158
Stephen Rauch Avatar answered Sep 22 '22 05:09

Stephen Rauch


Another possible approach is to define a custom decorator that adds the common argument/option and use this decorator on every subcommand. This way, click will not ask for the argument/option when issuing subcommand --help.

A possible implementation is outlined in click's GitHub Issue 295. The example code shows how to add a general purpose decorator which can add different - yet shared - options. It does however solve OP's issue.

like image 35
Rolf Avatar answered Sep 22 '22 05:09

Rolf