Python Click - only execute subcommand if parent command executed successfully
Asked Answered
R

1

7

I'm using Click to build a Python CLI and am running into an issue with how exceptions are handles in Click.

I'm not sure about the wording ("subcommand", "parentcommand") here but from my example you'll get the idea I hope. Let's assume this code:

@click.group()
@click.option("--something")
def mycli(something):
    try:
        #do something with "something" and set ctx
        ctx.obj = {}
        ctx.obj["somevar"] = some_result
    except:
        print("Something went wrong")
        raise

    #only if everything went fine call mycommand

@click.group()
@click.pass_context
def mygroup(ctx):
    pass

@mygroup.command(name="mycommand")
@click.pass_context
def mycommand(ctx):
    #this only works if somevar is set in ctx so don't call this if setting went wrong in mycli

When the application starts this is called:

if __name__ == "__main__":
    mycli.add_command(mygroup)
    mycli()

I then start the program like this:

python myapp --something somevalue mycommand

Expected behaviour: first mycli is called and the code in it is executed. If an exception is thrown it's caught by the except block, a message is printed and the exception is raised. Because we have no other try/except block this will result in termination of the script. The "sub"-command mycommand is never called because the program already terminated when running the "parent"-command mycli.

Actual behaviour: the exception is caughtand the message is printed, but mycommand is still called. It then fails with another exception message because the required context variable was not set.

How would I handle something like that? Basically I only want to call the subcommand mycommand only to be executed if everything in mycli went fine.

Rubber answered 14/8, 2018 at 18:23 Comment(0)
B
3

To handle the exception, but not continue onto the subcommands, you can simply call exit() like:

Code:

import click

@click.group()
@click.option("--something")
@click.pass_context
def mycli(ctx, something):
    ctx.obj = dict(a_var=something)
    try:
        if something != '1':
            raise IndexError('An Error')
    except Exception as exc:
        click.echo('Exception: {}'.format(exc))
        exit()

Test Code:

@mycli.group()
@click.pass_context
def mygroup(ctx):
    click.echo('mygroup: {}'.format(ctx.obj['a_var']))
    pass


@mygroup.command()
@click.pass_context
def mycommand(ctx):
    click.echo('mycommand: {}'.format(ctx.obj['a_var']))


if __name__ == "__main__":
    commands = (
        'mygroup mycommand',
        '--something 1 mygroup mycommand',
        '--something 2 mygroup mycommand',
        '--help',
        '--something 1 mygroup --help',
        '--something 1 mygroup mycommand --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)
            mycli(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)]
-----------
> mygroup mycommand
Exception: An Error
-----------
> --something 1 mygroup mycommand
mygroup: 1
mycommand: 1
-----------
> --something 2 mygroup mycommand
Exception: An Error
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --something TEXT
  --help            Show this message and exit.

Commands:
  mygroup
-----------
> --something 1 mygroup --help
Usage: test.py mygroup [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  mycommand
-----------
> --something 1 mygroup mycommand --help
mygroup: 1
Usage: test.py mygroup mycommand [OPTIONS]

Options:
  --help  Show this message and exit.
Biochemistry answered 15/8, 2018 at 0:16 Comment(6)
Thanks Stephen. That's what I'm doing currently but it won't work when digging down the rabbit hole deeper. E.g. let's just say I add another subcommand layer, let's call it sub-sub-command. And now I have an issue in my sub-command and want the exception to bubble up to the parent-command and the sub-sub-command never to be called. In that case I can't just use exit() in the subcommand.Rubber
Why can't you call exit()? What does calling exit() fail to achieve?Biochemistry
In my case - without going into details (would be a longer discussion) I want to bubble the exception up because I have a top level exception handler that will catch all exception if the CLI is used and wrap them into a JSON error message. If the CLI modules are used as an API then I don't want that behaviour. Without the bubbleing up I'd need to place the JSON exception wrapper in every subcommand which breaks DRY. I imagine there are other cases where bubbleing up is needed as well.Rubber
OK, so The question is really how do I do a top level exception handler. Have you tried this?Biochemistry
Sorry for the late replay. I saw this in the click docs already but first didn't like the idea to have a special handler. I'd rather stay to the default "bubble up" behaviour. However since there seems not to be such an option for now and it'll effectivly result in the same function, I'll give it a shot and let you know how it went. Thanks for your suggestions!Rubber
I tested your suggestion with a top level exception handler but it doesn't seem to work the way I'd need it. I added it to the click group and when the exception happens in a subcommand it works as expected. but my goal was to never call a subcommand when an exception happens in the group. for exceptions that occure in the group however the exception handler is never called and I can see that my subcommand is still called after the exception occures.Rubber

© 2022 - 2024 — McMap. All rights reserved.