Search code examples
pythoncommand-line-interfacepython-click

Python Click: Error: Missing argument while calling the --help flag


This is my code:

@click.group()
@click.pass_context
@click.argument('CHALLENGE', type=int)
def challenge(ctx, challenge):
    ctx.obj = Challenge(challenge=challenge)

@click.group(invoke_without_command=True, cls=PhaseGroup)
@click.pass_obj
@click.argument('PHASE', type=int)
def phase(ctx, phase):
    # Something

challenge.add_command(phase)

Together these commands are supposed to work like.

cli challenge 1 phase 1

Which works as intended by executing properly.

But when I use --help or define any other flags on phase, it throws

cli challenge 1 phase 1 --help

It throws Error: Missing argument "PHASE".

I searched SOF and found python click app is failing with indication of "missing argument" but cannot locate the issue This is a user who had the same issue, but I didn't quite understand the answer.

I cannot make the arguments optional since it is a crucial part of making the CLI work.


Solution

  • The fundamental problem your code is facing is that you are trying to use a group like a command. There are at least two ways to improve the situation:

    Use a command:

    The terminal enties are usually a click.Command not a click.Group. If you change phase to a click.Command the help will work as expected.

    @challenge.command()
    @click.argument('PHASE', type=int)
    def phase(ctx, phase):
        ....    
    

    Use no_args_is_help with the group:

    If you pass no_args_is_help=True to the click.Group constructor, the help will be displayed as you hoped. But unless you have a really good reason for this I would instead suggest using a click.Command as shown above.

    @challenge.group(invoke_without_command=True, no_args_is_help=True)
    @click.argument('PHASE', type=int)
    def phase(ctx, phase):
        ....
    

    Side Note, you don't usually need add_command():

    You do not generally need to use:

    @click.command()
    def my_command():
        ....
    challenge.add_command(my_command)        
    

    You can instead do:

    @challenge.command()
    def my_command():
        ....
    

    Test Code:

    import click
    
    @click.group()
    @click.pass_context
    @click.argument('CHALLENGE', type=int)
    def challenge(ctx, challenge):
        ctx.obj = challenge
    
    @challenge.command()
    @click.pass_obj
    @click.argument('PHASE', type=int)
    def phase1(ctx, phase):
        """Phase1 Command"""
        click.echo((phase))
    
    @challenge.group(invoke_without_command=True, no_args_is_help=True)
    @click.pass_obj
    @click.argument('PHASE', type=int)
    def phase2(ctx, phase):
        """Phase2 Group"""
        click.echo((phase))
    
    
    if __name__ == "__main__":
        commands = (
            '--help',
            '1 phase1',
            '1 phase1 --help',
            '1 phase1 2 ',
            '1 phase1 2 --help',
            '1 phase2',
            '1 phase2 --help',
            '1 phase2 2 ',
            '1 phase2 2 --help',
            '--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)
                challenge(cmd.split())
    
            except BaseException as exc:
                if str(exc) != '0' and \
                        not isinstance(exc, (click.ClickException, SystemExit)):
                    raise
    

    Test 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)]
    -----------
    > --help
    Usage: test.py [OPTIONS] CHALLENGE COMMAND [ARGS]...
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      phase1  Phase1 Command
      phase2  Phase2 Group
    -----------
    > 1 phase1
    Usage: test.py phase1 [OPTIONS] PHASE
    
    Error: Missing argument "PHASE".
    -----------
    > 1 phase1 --help
    Usage: test.py phase1 [OPTIONS] PHASE
    
      Phase1 Command
    
    Options:
      --help  Show this message and exit.
    -----------
    > 1 phase1 2 
    2
    -----------
    > 1 phase1 2 --help
    Usage: test.py phase1 [OPTIONS] PHASE
    
      Phase1 Command
    
    Options:
      --help  Show this message and exit.
    -----------
    > 1 phase2
    Usage: test.py phase2 [OPTIONS] PHASE COMMAND [ARGS]...
    
      Phase2 Group
    
    Options:
      --help  Show this message and exit.
    -----------
    > 1 phase2 --help
    Usage: test.py phase2 [OPTIONS] PHASE COMMAND [ARGS]...
    
      Phase2 Group
    
    Options:
      --help  Show this message and exit.
    -----------
    > 1 phase2 2 
    2
    -----------
    > 1 phase2 2 --help
    Usage: test.py phase2 [OPTIONS] PHASE COMMAND [ARGS]...
    
      Phase2 Group
    
    Options:
      --help  Show this message and exit.
    -----------
    > --help
    Usage: test.py [OPTIONS] CHALLENGE COMMAND [ARGS]...
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      phase1  Phase1 Command
      phase2  Phase2 Group