2

So here's my issue:

Assume that I have built a CLI using Python Click for which I have created custom classes of groups and commands that wrap invoking to catch exceptions:

logger = logging.getLogger()
class CLICommandInvoker(click.Command):
    def invoke(self, ctx):
        command = ctx.command.name
        try:
            logger.info("Running {command} command".format(command=command))
            ret = super(CLICommandInvoker, self).invoke(ctx)
            logger.info("Completed {command} command".format(command=command))
            return ret

        except Exception as exc:
            logger.error(
                'Command {command} failed with exception: {exc}'.format(command=command, exc=exc)
            )

            """ In case command invoked from another command """
            raise Exception("Failed to invoke {command} command".format(command=command))


class CLIGroupInvoker(click.Group):
    def invoke(self, ctx):
        group = ctx.command.name
        try:
            ret = super(CLIGroupInvoker, self).invoke(ctx)
            group_subcommand = ctx.invoked_subcommand
            logger.info(
                'Command "{group}-{subcommand}" completed successfully'.format(group = group, subcommand = group_subcommand)
            )
            return ret

        except Exception:
            group_subcommand = ctx.invoked_subcommand
            logger.error(
                'Command "{group}-{subcommand}" failed'.format(group=group, subcommand=group_subcommand)
            )

Now, for example I have two commands in a certain group:

@click.group(cls=CLIGroupInvoker)
def g():
    pass

@g.command(cls=CLICommandInvoker)
def c1():
    print("C1")

@g.command(cls=CLICommandInvoker)
@click.pass_context
def c2(ctx):
    ctx.invoke(c1)
    print("C2")

So, the code runs fine, but the invoke method of the context in c2 does not run the custom invoke in my CLICommandInvoker, but goes straight to the c1 function instead. I don't see the Running c1 command or other logs that are in the custom invoke regarding c1, only those about c2.

So, what am I doing wrong here? How can I have the command invocation use the custom class when invoking commands from another command? Or is that not possible?

I know there is a solution to simply refactor the code to extract the implementation itself and simply have the commands "wrap" the actual logic, but let's say that for the moment it's not possible.

1 Answer 1

3

The trouble you are running into, is that you are calling click.Context.invoke, which does not use the click.Command.invoke. With a little DRY we can factor out your invoke wrapper and use it like:

Code:

def invoke_with_catch(self, ctx, original_invoke):

    fmt = dict(command=getattr(ctx, 'command', ctx).name)
    try:
        click.echo("Running {command} command".format(**fmt))
        result = original_invoke(self, ctx)
        click.echo("Completed {command} command".format(**fmt))
        return result

    except Exception as exc:
        click.echo(
            'Command {command} failed with exception: {exc}'.format(
                exc=exc, **fmt)
        )

        """ In case command invoked from another command """
        raise click.ClickException(
            "Failed to invoke {command} command".format(**fmt))

Calling the Code:

The wrapper can be called directly like:

invoke_with_catch(ctx, c1, click.Context.invoke)

or can be used in the inherited class like:

class CLICommandInvoker(click.Command):
    def invoke(self, ctx):
        return invoke_with_catch(self, ctx, click.Command.invoke)

Test Code:

import click

class CLICommandInvoker(click.Command):
    def invoke(self, ctx):
        return invoke_with_catch(self, ctx, click.Command.invoke)


class CLIGroupInvoker(click.Group):
    def invoke(self, ctx):
        return invoke_with_catch(self, ctx, click.Group.invoke)


@click.group(cls=CLIGroupInvoker)
def g():
    pass

@g.command(cls=CLICommandInvoker)
@click.option("--throw", is_flag=True)
def c1(throw):
    click.echo("C1")
    if throw:
        raise Exception('Throwing in C1')

@g.command(cls=CLICommandInvoker)
@click.option("--throw", is_flag=True)
@click.pass_context
def c2(ctx, throw):
    invoke_with_catch(ctx, c1, click.Context.invoke)
    click.echo("C2")
    if throw:
        raise Exception('Throwing in C2')


if __name__ == "__main__":
    commands = (
        'c1',
        'c1 --throw',
        'c2',
        'c2 --throw',
        '--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)
            g(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)]
-----------
> c1
Running g command
Running c1 command
C1
Completed c1 command
Completed g command
-----------
> c1 --throw
Running g command
Running c1 command
C1
Command c1 failed with exception: Throwing in C1
Command g failed with exception: Failed to invoke c1 command
Error: Failed to invoke g command
-----------
> c2
Running g command
Running c2 command
Running c1 command
C1
Completed c1 command
C2
Completed c2 command
Completed g command
-----------
> c2 --throw
Running g command
Running c2 command
Running c1 command
C1
Completed c1 command
C2
Command c2 failed with exception: Throwing in C2
Command g failed with exception: Failed to invoke c2 command
Error: Failed to invoke g command
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  c1
  c2
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  c1
  c2
Sign up to request clarification or add additional context in comments.

1 Comment

That works great, but starts getting a bit tricky when invoking commands with arguments (Such as giving --throw when invoking c1 in c2). Any way to include keyword arguments (The way Context takes) to them as well?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.