Python 单击:在具有上下文资源的链式 MultiCommand 中处理 cli 使用异常
Python Click: handling cli usage Exceptions in a chained MultiCommand with context resource
背景
Click 文档中的以下示例(具体来说 custom multi commands, multi command pipelines and managing resources) I've written a CLI application similar to the Image Pipeline example 仅它通过 FBX SDK 而不是图像在 3D 网格场景上运行。
抛开那个特定的细节,我很难理解 Click 对命令行的解析是如何发生的以及何时发生的。我的问题的 TLDR 版本是命令行使用错误似乎只在上下文退出后出现,重要的是,在关闭任何上下文管理的资源后。理想情况下,我想在进入上下文管理器之前确定命令行是否有效,或者至少能够在管理器退出之前对使用错误做出反应。
最小示例如下:
- 链接的多命令设置为
input
和 output
文件路径选项。
- 上下文管理器用于 load/save 数据(对于这个例子,我只是使用
dict
和 json
文件作为 3D 场景数据和 FBX 文件的替代品) .
- 此上下文管理器与 Clicks
ctx.with_resource
方法一起使用并存储在上下文用户对象中。
- 子命令都通过上下文并有自己的任意选项(对于这个例子,它们都只是编辑
dict
值并且在同一个脚本中,在实际实现中它们是作为插件实现的自定义多命令示例)
- 所有子命令都是生成器并产生上下文,这些生成器由
process
函数使用 运行 作为点击回调的结果。 (类似于多命令管道示例)
示例代码
import click
import json
@click.command
@click.option("-t", "value", type=float)
@click.pass_context
def translate(ctx, value):
click.echo(f"modifying: 'translate' by {value}")
ctx.obj.data["translate"] += value
yield ctx
@click.command
@click.option("-r", "value", type=float)
@click.pass_context
def rotate(ctx, value):
click.echo(f"modifying: 'rotate' by {value}")
ctx.obj.data["rotate"] += value
yield ctx
@click.command
@click.option("-s", "value", type=float)
@click.pass_context
def scale(ctx, value):
click.echo(f"modifying: 'scale' by {value}")
ctx.obj.data["scale"] += value
yield ctx
@click.command
@click.pass_context
def report(ctx):
click.echo(f"object data: {ctx.obj.data}")
click.echo(f"object in: {ctx.obj.infile}")
click.echo(f"object out: {ctx.obj.outfile}")
yield ctx
class EditModelCLI(click.MultiCommand):
def list_commands(self, ctx):
return ["translate", "rotate", "scale", "report"]
def get_command(self, ctx, name):
return globals()[name]
class FileResource:
def __init__(self, infile, outfile=None):
self.infile = infile
self.outfile = outfile
self._dict = {}
@property
def data(self):
return self._dict
def __enter__(self):
if self.infile:
with open(self.infile) as f:
click.echo(f"loading {self.infile}")
self._dict = json.load(f)
return self
def __exit__(self, exc_type, exc_value, tb):
if self.outfile:
with open(self.outfile, "w") as f:
click.echo(f"saving {self.outfile}")
json.dump(self._dict, f)
@click.option("-i", "--input", type=click.Path(exists=True, dir_okay=False))
@click.option("-o", "--output", type=click.Path(), default=None)
@click.group(cls=EditModelCLI, chain=True, no_args_is_help=True)
@click.pass_context
def main(ctx, input, output):
click.echo("starting process")
ctx.obj = ctx.with_resource(FileResource(input, output))
@main.result_callback()
def process(subcommands, **kwargs):
for cmd in subcommands:
result = next(cmd)
click.echo(f"processed ... {result.info_name}")
if __name__ == "__main__":
click.echo("called from commandline")
main()
测试
所以这给了我们一个 cli,我们可以在其中调用:
> python -m click_test.py -i model_in.json -o model_out.json translate -t 1.0 scale -s 0.5
读入 - model_in.json
{"translate": 1.0, "rotate": 1.0, "scale": 1.0}
并写出 - model_out.json
{"translate": 2.0, "rotate": 1.0, "scale": 1.5}
到目前为止按设计工作,如果 model_out.json
已经存在,则创建或修改它。
然而,任何命令行语法错误(在子命令级别)仍会导致“输出”文件被写出,托管资源仍会打开和关闭。所以调用:
> python -m click_test.py -i model_in.json -o model_out.json translate -t 1.0 scale -foobar 0.5
scale
的选项中有错误仍会写出 - model_out.json
没有对文件进行任何更改,我们还没有处理 translate
子命令。
我正在尝试确定如何或在何处可以在上下文退出之前捕获任何子命令使用错误,以便我可以访问 FileResource
并防止保存。
eg ctx.obj.outfile = None
可以实现这个,我只是不知道在哪里可以检测到使用错误以便调用它。
您可以在资源处理程序 __exit__()
中测试程序退出代码,例如:
def __exit__(self, exc_type, exc_value, tb):
sys_exc = sys.exc_info()[1]
if isinstance(sys_exc, click.exceptions.Exit) and sys_exc.exit_code == 0:
# Only execute this on a successful exit
click.echo(f"Executing {type(self).__name__} __exit__()")
完整示例:
import click
@click.command
@click.option("--value", type=float)
@click.pass_context
def command(ctx, value):
click.echo(f"command got {value}")
yield ctx
class OurCLI(click.MultiCommand):
def list_commands(self, ctx):
return ["command"]
def get_command(self, ctx, name):
return globals()[name]
class OurResource:
def __init__(self, ctx):
self.ctx = ctx
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
sys_exc = sys.exc_info()[1]
if isinstance(sys_exc, click.exceptions.Exit) and sys_exc.exit_code == 0:
# Only execute this on a successful exit
click.echo(f"Executing {type(self).__name__} __exit__()")
@click.group(cls=OurCLI, chain=True)
@click.pass_context
def main(ctx):
click.echo("main")
ctx.obj = ctx.with_resource(OurResource(ctx))
测试代码:
if __name__ == "__main__":
commands = (
'command --value 5',
'command --value XX',
'--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)
main(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
结果:
Click Version: 8.1.3
Python Version: 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)]
-----------
> command --value 5
main
Executing OurResource __exit__()
-----------
> command --value XX
main
Usage: test_code.py command [OPTIONS]
Try 'test_code.py command --help' for help.
Error: Invalid value for '--value': 'XX' is not a valid float.
-----------
> --help
Usage: test_code.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--help Show this message and exit.
Commands:
command
背景
Click 文档中的以下示例(具体来说 custom multi commands, multi command pipelines and managing resources) I've written a CLI application similar to the Image Pipeline example 仅它通过 FBX SDK 而不是图像在 3D 网格场景上运行。
抛开那个特定的细节,我很难理解 Click 对命令行的解析是如何发生的以及何时发生的。我的问题的 TLDR 版本是命令行使用错误似乎只在上下文退出后出现,重要的是,在关闭任何上下文管理的资源后。理想情况下,我想在进入上下文管理器之前确定命令行是否有效,或者至少能够在管理器退出之前对使用错误做出反应。
最小示例如下:
- 链接的多命令设置为
input
和output
文件路径选项。 - 上下文管理器用于 load/save 数据(对于这个例子,我只是使用
dict
和json
文件作为 3D 场景数据和 FBX 文件的替代品) . - 此上下文管理器与 Clicks
ctx.with_resource
方法一起使用并存储在上下文用户对象中。 - 子命令都通过上下文并有自己的任意选项(对于这个例子,它们都只是编辑
dict
值并且在同一个脚本中,在实际实现中它们是作为插件实现的自定义多命令示例) - 所有子命令都是生成器并产生上下文,这些生成器由
process
函数使用 运行 作为点击回调的结果。 (类似于多命令管道示例)
示例代码
import click
import json
@click.command
@click.option("-t", "value", type=float)
@click.pass_context
def translate(ctx, value):
click.echo(f"modifying: 'translate' by {value}")
ctx.obj.data["translate"] += value
yield ctx
@click.command
@click.option("-r", "value", type=float)
@click.pass_context
def rotate(ctx, value):
click.echo(f"modifying: 'rotate' by {value}")
ctx.obj.data["rotate"] += value
yield ctx
@click.command
@click.option("-s", "value", type=float)
@click.pass_context
def scale(ctx, value):
click.echo(f"modifying: 'scale' by {value}")
ctx.obj.data["scale"] += value
yield ctx
@click.command
@click.pass_context
def report(ctx):
click.echo(f"object data: {ctx.obj.data}")
click.echo(f"object in: {ctx.obj.infile}")
click.echo(f"object out: {ctx.obj.outfile}")
yield ctx
class EditModelCLI(click.MultiCommand):
def list_commands(self, ctx):
return ["translate", "rotate", "scale", "report"]
def get_command(self, ctx, name):
return globals()[name]
class FileResource:
def __init__(self, infile, outfile=None):
self.infile = infile
self.outfile = outfile
self._dict = {}
@property
def data(self):
return self._dict
def __enter__(self):
if self.infile:
with open(self.infile) as f:
click.echo(f"loading {self.infile}")
self._dict = json.load(f)
return self
def __exit__(self, exc_type, exc_value, tb):
if self.outfile:
with open(self.outfile, "w") as f:
click.echo(f"saving {self.outfile}")
json.dump(self._dict, f)
@click.option("-i", "--input", type=click.Path(exists=True, dir_okay=False))
@click.option("-o", "--output", type=click.Path(), default=None)
@click.group(cls=EditModelCLI, chain=True, no_args_is_help=True)
@click.pass_context
def main(ctx, input, output):
click.echo("starting process")
ctx.obj = ctx.with_resource(FileResource(input, output))
@main.result_callback()
def process(subcommands, **kwargs):
for cmd in subcommands:
result = next(cmd)
click.echo(f"processed ... {result.info_name}")
if __name__ == "__main__":
click.echo("called from commandline")
main()
测试
所以这给了我们一个 cli,我们可以在其中调用:
> python -m click_test.py -i model_in.json -o model_out.json translate -t 1.0 scale -s 0.5
读入 - model_in.json
{"translate": 1.0, "rotate": 1.0, "scale": 1.0}
并写出 - model_out.json
{"translate": 2.0, "rotate": 1.0, "scale": 1.5}
到目前为止按设计工作,如果 model_out.json
已经存在,则创建或修改它。
然而,任何命令行语法错误(在子命令级别)仍会导致“输出”文件被写出,托管资源仍会打开和关闭。所以调用:
> python -m click_test.py -i model_in.json -o model_out.json translate -t 1.0 scale -foobar 0.5
scale
的选项中有错误仍会写出 - model_out.json
没有对文件进行任何更改,我们还没有处理 translate
子命令。
我正在尝试确定如何或在何处可以在上下文退出之前捕获任何子命令使用错误,以便我可以访问 FileResource
并防止保存。
eg ctx.obj.outfile = None
可以实现这个,我只是不知道在哪里可以检测到使用错误以便调用它。
您可以在资源处理程序 __exit__()
中测试程序退出代码,例如:
def __exit__(self, exc_type, exc_value, tb):
sys_exc = sys.exc_info()[1]
if isinstance(sys_exc, click.exceptions.Exit) and sys_exc.exit_code == 0:
# Only execute this on a successful exit
click.echo(f"Executing {type(self).__name__} __exit__()")
完整示例:
import click
@click.command
@click.option("--value", type=float)
@click.pass_context
def command(ctx, value):
click.echo(f"command got {value}")
yield ctx
class OurCLI(click.MultiCommand):
def list_commands(self, ctx):
return ["command"]
def get_command(self, ctx, name):
return globals()[name]
class OurResource:
def __init__(self, ctx):
self.ctx = ctx
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
sys_exc = sys.exc_info()[1]
if isinstance(sys_exc, click.exceptions.Exit) and sys_exc.exit_code == 0:
# Only execute this on a successful exit
click.echo(f"Executing {type(self).__name__} __exit__()")
@click.group(cls=OurCLI, chain=True)
@click.pass_context
def main(ctx):
click.echo("main")
ctx.obj = ctx.with_resource(OurResource(ctx))
测试代码:
if __name__ == "__main__":
commands = (
'command --value 5',
'command --value XX',
'--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)
main(cmd.split())
except BaseException as exc:
if str(exc) != '0' and \
not isinstance(exc, (click.ClickException, SystemExit)):
raise
结果:
Click Version: 8.1.3
Python Version: 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)]
-----------
> command --value 5
main
Executing OurResource __exit__()
-----------
> command --value XX
main
Usage: test_code.py command [OPTIONS]
Try 'test_code.py command --help' for help.
Error: Invalid value for '--value': 'XX' is not a valid float.
-----------
> --help
Usage: test_code.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...
Options:
--help Show this message and exit.
Commands:
command