使用 Python 保留用户提供的参数的顺序 单击
Preserving the order of user-provided parameters with Python Click
我正在尝试将 argparse
命令行界面 (CLI) 移植到 click
。此 CLI 必须维护用户提供参数的顺序。对于 argparse
版本,我使用 this Whosebug answer 来维持秩序。使用 click
,我不知道该怎么做。
我尝试创建一个自定义回调来按顺序存储参数和值,但如果多次使用某个参数,回调会在它第一次看到匹配参数时触发。
import click
import typing
class OrderParams:
_options: typing.List[typing.Tuple[click.Parameter, typing.Any]] = []
@classmethod
def append(cls, ctx: click.Context, param: click.Parameter, value: typing.Any):
cls._options.append((param, value))
@click.command()
@click.option("--animal", required=True, multiple=True, callback=OrderParams.append)
@click.option("--thing", required=True, multiple=True, callback=OrderParams.append)
def cli(*, animal, thing):
click.echo("Got this order of parameters:")
for param, value in OrderParams._options:
print(" ", param.name, value)
if __name__ == "__main__":
cli()
当前输出:
$ python cli.py --animal cat --thing rock --animal dog
Got this order of parameters:
animal ('cat', 'dog')
thing ('rock',)
期望的输出:
$ python cli.py --animal cat --thing rock --animal dog
Got this order of parameters:
animal 'cat'
thing 'rock'
animal 'dog'
click documentation describes the behavior第一次遇到参数时调用一次回调,即使多次使用该参数。
If an option or argument is split up on the command line into multiple places because it is repeated [...] the callback will fire based on the position of the first option.
这可以通过使用自定义 class 覆盖 click.Command
参数解析器调用来完成,例如:
自定义Class:
class OrderedParamsCommand(click.Command):
_options = []
def parse_args(self, ctx, args):
# run the parser for ourselves to preserve the passed order
parser = self.make_parser(ctx)
opts, _, param_order = parser.parse_args(args=list(args))
for param in param_order:
type(self)._options.append((param, opts[param.name].pop(0)))
# return "normal" parse results
return super().parse_args(ctx, args)
使用自定义 Class:
然后要使用自定义命令,将其作为 cls
参数传递给 command
装饰器,例如:
@click.command(cls=OrderedParamsCommand)
@click.option("--animal", required=True, multiple=True)
@click.option("--thing", required=True, multiple=True)
def cli(*, animal, thing):
....
这是如何工作的?
之所以有效,是因为 click
是一个设计良好的 OO 框架。 @click.command()
装饰器
通常实例化一个 click.Command
对象,但允许这种行为被覆盖
cls
参数。所以从我们的 click.Command
继承是一件相对容易的事情
拥有 class 并超越所需的方法。
在这种情况下,我们自己超越 click.Command.parse_args()
和 运行 解析器以保留
订单已通过。
测试代码:
import click
class OrderedParamsCommand(click.Command):
_options = []
def parse_args(self, ctx, args):
parser = self.make_parser(ctx)
opts, _, param_order = parser.parse_args(args=list(args))
for param in param_order:
type(self)._options.append((param, opts[param.name].pop(0)))
return super().parse_args(ctx, args)
@click.command(cls=OrderedParamsCommand)
@click.option("--animal", required=True, multiple=True)
@click.option("--thing", required=True, multiple=True)
def cli(*, animal, thing):
click.echo("Got this order of parameters:")
for param, value in OrderedParamsCommand._options:
print(" ", param.name, value)
if __name__ == "__main__":
cli('--animal cat --thing rock --animal dog'.split())
结果:
Got this order of parameters:
animal cat
thing rock
animal dog
@StephenRauch 的回答是正确的,但遗漏了一个小细节。调用 cli
函数,即不带任何参数的脚本将导致错误:
type(self)._options.append((param, opts[param.name].pop(0)))
AttributeError: 'NoneType' object has no attribute 'pop'
解决此问题的方法是检查 opts[param.name]
是否不是 None
。修改后的 OrderedParamsCommand
看起来像:
class OrderedParamsCommand(click.Command):
_options = []
def parse_args(self, ctx, args):
# run the parser for ourselves to preserve the passed order
parser = self.make_parser(ctx)
opts, _, param_order = parser.parse_args(args=list(args))
for param in param_order:
# Type check
option = opts[param.name]
if option != None:
type(self)._options.append((param, option.pop(0)))
# return "normal" parse results
return super().parse_args(ctx, args)
编辑:我发布了一个答案,因为我无法提出修改建议
编辑 2:呃,这不应该是解决方案。我刚刚尝试了 --help
,它因以下错误而崩溃:
type(self)._options.append((param, option.pop(0)))
AttributeError: 'bool' object has no attribute 'pop'
太糟糕了,我希望有一个合适的解决方案
我正在尝试将 argparse
命令行界面 (CLI) 移植到 click
。此 CLI 必须维护用户提供参数的顺序。对于 argparse
版本,我使用 this Whosebug answer 来维持秩序。使用 click
,我不知道该怎么做。
我尝试创建一个自定义回调来按顺序存储参数和值,但如果多次使用某个参数,回调会在它第一次看到匹配参数时触发。
import click
import typing
class OrderParams:
_options: typing.List[typing.Tuple[click.Parameter, typing.Any]] = []
@classmethod
def append(cls, ctx: click.Context, param: click.Parameter, value: typing.Any):
cls._options.append((param, value))
@click.command()
@click.option("--animal", required=True, multiple=True, callback=OrderParams.append)
@click.option("--thing", required=True, multiple=True, callback=OrderParams.append)
def cli(*, animal, thing):
click.echo("Got this order of parameters:")
for param, value in OrderParams._options:
print(" ", param.name, value)
if __name__ == "__main__":
cli()
当前输出:
$ python cli.py --animal cat --thing rock --animal dog
Got this order of parameters:
animal ('cat', 'dog')
thing ('rock',)
期望的输出:
$ python cli.py --animal cat --thing rock --animal dog
Got this order of parameters:
animal 'cat'
thing 'rock'
animal 'dog'
click documentation describes the behavior第一次遇到参数时调用一次回调,即使多次使用该参数。
If an option or argument is split up on the command line into multiple places because it is repeated [...] the callback will fire based on the position of the first option.
这可以通过使用自定义 class 覆盖 click.Command
参数解析器调用来完成,例如:
自定义Class:
class OrderedParamsCommand(click.Command):
_options = []
def parse_args(self, ctx, args):
# run the parser for ourselves to preserve the passed order
parser = self.make_parser(ctx)
opts, _, param_order = parser.parse_args(args=list(args))
for param in param_order:
type(self)._options.append((param, opts[param.name].pop(0)))
# return "normal" parse results
return super().parse_args(ctx, args)
使用自定义 Class:
然后要使用自定义命令,将其作为 cls
参数传递给 command
装饰器,例如:
@click.command(cls=OrderedParamsCommand)
@click.option("--animal", required=True, multiple=True)
@click.option("--thing", required=True, multiple=True)
def cli(*, animal, thing):
....
这是如何工作的?
之所以有效,是因为 click
是一个设计良好的 OO 框架。 @click.command()
装饰器
通常实例化一个 click.Command
对象,但允许这种行为被覆盖
cls
参数。所以从我们的 click.Command
继承是一件相对容易的事情
拥有 class 并超越所需的方法。
在这种情况下,我们自己超越 click.Command.parse_args()
和 运行 解析器以保留
订单已通过。
测试代码:
import click
class OrderedParamsCommand(click.Command):
_options = []
def parse_args(self, ctx, args):
parser = self.make_parser(ctx)
opts, _, param_order = parser.parse_args(args=list(args))
for param in param_order:
type(self)._options.append((param, opts[param.name].pop(0)))
return super().parse_args(ctx, args)
@click.command(cls=OrderedParamsCommand)
@click.option("--animal", required=True, multiple=True)
@click.option("--thing", required=True, multiple=True)
def cli(*, animal, thing):
click.echo("Got this order of parameters:")
for param, value in OrderedParamsCommand._options:
print(" ", param.name, value)
if __name__ == "__main__":
cli('--animal cat --thing rock --animal dog'.split())
结果:
Got this order of parameters:
animal cat
thing rock
animal dog
@StephenRauch 的回答是正确的,但遗漏了一个小细节。调用 cli
函数,即不带任何参数的脚本将导致错误:
type(self)._options.append((param, opts[param.name].pop(0)))
AttributeError: 'NoneType' object has no attribute 'pop'
解决此问题的方法是检查 opts[param.name]
是否不是 None
。修改后的 OrderedParamsCommand
看起来像:
class OrderedParamsCommand(click.Command):
_options = []
def parse_args(self, ctx, args):
# run the parser for ourselves to preserve the passed order
parser = self.make_parser(ctx)
opts, _, param_order = parser.parse_args(args=list(args))
for param in param_order:
# Type check
option = opts[param.name]
if option != None:
type(self)._options.append((param, option.pop(0)))
# return "normal" parse results
return super().parse_args(ctx, args)
编辑:我发布了一个答案,因为我无法提出修改建议
编辑 2:呃,这不应该是解决方案。我刚刚尝试了 --help
,它因以下错误而崩溃:
type(self)._options.append((param, option.pop(0)))
AttributeError: 'bool' object has no attribute 'pop'
太糟糕了,我希望有一个合适的解决方案