Reputation: 1246
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.
Upvotes: 2
Views: 3746
Reputation: 49814
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:
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))
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)
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
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
Upvotes: 3