Python 单击 - 即使在组命令中引发异常,也显示子命令的帮助
Python Click - display help for subcommand even if exception is raised in group command
我正在编写一个命令行脚本,mycli
,有两个子命令:
mkcli init
(用.configrc
文件初始化一个空项目)
mkcli run
(到运行脚本的主要逻辑)。
一般来说,如果在工作目录中找不到 .configrc
文件,mycli run
应该不起作用。但是,我的用户应该能够查看 帮助消息 for run
:
$ mycli run --help
Usage: mycli run [OPTIONS]
Options:
--dryrun Run in read-only mode
--help Show this message and exit.
但是,如果 .configrc
不存在,这将不起作用,因为 FileNotFoundError
在组命令 cli
中引发(并且从未达到 run
)。通过使用 ctx.invoked_subcommand
(见下文),我可以在不首先找到 .configrc
文件的情况下触发 init
子命令,但我看不出有什么办法可以确保 run
子命令将如果使用 --help
.
调用,则始终触发
如果用户 运行s mkcli run
并且没有找到 .configrc
文件,我的脚本将以 run "mycli init" first
退出。但是 mycli run --help
即使没有 .configrc
也应该可以工作。我怎样才能做到这一点?或者谁能建议更好的方法来处理 init
?
@click.group()
@click.pass_context
def cli(ctx):
ctx.obj = {}
if ctx.invoked_subcommand != "init":
config = yaml.load(open(".configrc").read())
ctx.obj.update({key: config[key] for key in config})
@cli.command()
@click.pass_context
def init(ctx):
print("Initialize project.")
@cli.command()
@click.option("--dryrun", type=bool, is_flag=True, help="Run in read-only mode")
@click.pass_context
def run(ctx, dryrun):
print("Run main program here.")
我建议更改初始代码的顺序 运行。这可以通过...
自定义 Class:
class LoadInitForCommands(click.Group):
def command(self, *args, **kwargs):
def decorator(f):
# call the original decorator
cmd = click.command(*args, **kwargs)(f)
self.add_command(cmd)
orig_invoke = cmd.invoke
def invoke(ctx):
# Custom init code is here
ctx.obj = {}
if cmd.name != "init":
config = yaml.load(open(".configrc").read())
ctx.obj.update({key: config[key] for key in config})
# call the original invoke()
return orig_invoke(ctx)
# hook the command's invoke
cmd.invoke = invoke
return cmd
return decorator
使用自定义 Class:
使用 cls
参数将自定义 Class 传递给 click.group()
,例如:
@click.group(cls=LoadInitForCommands)
def cli():
""""""
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。 @click.group()
装饰器通常实例化一个 click.Group
对象,但允许使用 cls
参数覆盖此行为。因此,在我们自己的 class 中继承 click.Group
并覆盖所需的方法是一件相对容易的事情。
在这种情况下,我们挂钩 command()
装饰器,并在该挂钩中覆盖命令的 invoke()
。这允许 在 已经处理 --help
标志之后读取初始化文件。
请注意,此代码旨在让许多命令变得容易,这些命令在读取 init 之前可用 --help
。在问题的示例中,只有一个命令需要 init.如果情况总是如此,那么 可能很有吸引力。
测试代码:
import click
import yaml
@click.group(cls=LoadInitForCommands)
def cli():
""""""
@cli.command()
@click.pass_context
def init(ctx):
print("Initialize project.")
@cli.command()
@click.option("--dryrun", type=bool, is_flag=True,
help="Run in read-only mode")
@click.pass_context
def run(ctx, dryrun):
print("Run main program here.")
if __name__ == "__main__":
commands = (
'init',
'run --help',
'run',
'--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)
cli(cmd.split(), obj={})
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)]
-----------
> init
Initialize project.
-----------
> run --help
Usage: test.py run [OPTIONS]
Options:
--dryrun Run in read-only mode
--help Show this message and exit.
-----------
> run
Traceback (most recent call last):
File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1741, in <module>
main()
File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1735, in main
globals = debugger.run(setup['file'], None, None, is_module)
File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1135, in run
pydev_imports.execfile(file, globals, locals) # execute the script
File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
exec(compile(contents+"\n", file, 'exec'), glob, loc)
File "C:/Users/stephen/Documents/src/testcode/test.py", line 77, in <module>
cli(cmd.split(), obj={})
File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 722, in __call__
return self.main(*args, **kwargs)
File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 697, in main
rv = self.invoke(ctx)
File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 1066, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "C:/Users/stephen/Documents/src/testcode/test.py", line 26, in invoke
config = yaml.load(open(".configrc").read())
FileNotFoundError: [Errno 2] No such file or directory: '.configrc'
我正在编写一个命令行脚本,mycli
,有两个子命令:
mkcli init
(用.configrc
文件初始化一个空项目)mkcli run
(到运行脚本的主要逻辑)。
一般来说,如果在工作目录中找不到 .configrc
文件,mycli run
应该不起作用。但是,我的用户应该能够查看 帮助消息 for run
:
$ mycli run --help
Usage: mycli run [OPTIONS]
Options:
--dryrun Run in read-only mode
--help Show this message and exit.
但是,如果 .configrc
不存在,这将不起作用,因为 FileNotFoundError
在组命令 cli
中引发(并且从未达到 run
)。通过使用 ctx.invoked_subcommand
(见下文),我可以在不首先找到 .configrc
文件的情况下触发 init
子命令,但我看不出有什么办法可以确保 run
子命令将如果使用 --help
.
如果用户 运行s mkcli run
并且没有找到 .configrc
文件,我的脚本将以 run "mycli init" first
退出。但是 mycli run --help
即使没有 .configrc
也应该可以工作。我怎样才能做到这一点?或者谁能建议更好的方法来处理 init
?
@click.group()
@click.pass_context
def cli(ctx):
ctx.obj = {}
if ctx.invoked_subcommand != "init":
config = yaml.load(open(".configrc").read())
ctx.obj.update({key: config[key] for key in config})
@cli.command()
@click.pass_context
def init(ctx):
print("Initialize project.")
@cli.command()
@click.option("--dryrun", type=bool, is_flag=True, help="Run in read-only mode")
@click.pass_context
def run(ctx, dryrun):
print("Run main program here.")
我建议更改初始代码的顺序 运行。这可以通过...
自定义 Class:
class LoadInitForCommands(click.Group):
def command(self, *args, **kwargs):
def decorator(f):
# call the original decorator
cmd = click.command(*args, **kwargs)(f)
self.add_command(cmd)
orig_invoke = cmd.invoke
def invoke(ctx):
# Custom init code is here
ctx.obj = {}
if cmd.name != "init":
config = yaml.load(open(".configrc").read())
ctx.obj.update({key: config[key] for key in config})
# call the original invoke()
return orig_invoke(ctx)
# hook the command's invoke
cmd.invoke = invoke
return cmd
return decorator
使用自定义 Class:
使用 cls
参数将自定义 Class 传递给 click.group()
,例如:
@click.group(cls=LoadInitForCommands)
def cli():
""""""
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。 @click.group()
装饰器通常实例化一个 click.Group
对象,但允许使用 cls
参数覆盖此行为。因此,在我们自己的 class 中继承 click.Group
并覆盖所需的方法是一件相对容易的事情。
在这种情况下,我们挂钩 command()
装饰器,并在该挂钩中覆盖命令的 invoke()
。这允许 在 已经处理 --help
标志之后读取初始化文件。
请注意,此代码旨在让许多命令变得容易,这些命令在读取 init 之前可用 --help
。在问题的示例中,只有一个命令需要 init.如果情况总是如此,那么
测试代码:
import click
import yaml
@click.group(cls=LoadInitForCommands)
def cli():
""""""
@cli.command()
@click.pass_context
def init(ctx):
print("Initialize project.")
@cli.command()
@click.option("--dryrun", type=bool, is_flag=True,
help="Run in read-only mode")
@click.pass_context
def run(ctx, dryrun):
print("Run main program here.")
if __name__ == "__main__":
commands = (
'init',
'run --help',
'run',
'--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)
cli(cmd.split(), obj={})
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)]
-----------
> init
Initialize project.
-----------
> run --help
Usage: test.py run [OPTIONS]
Options:
--dryrun Run in read-only mode
--help Show this message and exit.
-----------
> run
Traceback (most recent call last):
File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1741, in <module>
main()
File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1735, in main
globals = debugger.run(setup['file'], None, None, is_module)
File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\pydevd.py", line 1135, in run
pydev_imports.execfile(file, globals, locals) # execute the script
File "C:\Users\stephen\AppData\Local\JetBrains\PyCharm 2018.3\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
exec(compile(contents+"\n", file, 'exec'), glob, loc)
File "C:/Users/stephen/Documents/src/testcode/test.py", line 77, in <module>
cli(cmd.split(), obj={})
File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 722, in __call__
return self.main(*args, **kwargs)
File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 697, in main
rv = self.invoke(ctx)
File "C:\Users\stephen\AppData\Local\Programs\Python\Python36\lib\site-packages\click\core.py", line 1066, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "C:/Users/stephen/Documents/src/testcode/test.py", line 26, in invoke
config = yaml.load(open(".configrc").read())
FileNotFoundError: [Errno 2] No such file or directory: '.configrc'