【问题标题】:Call a click command from code从代码中调用点击命令
【发布时间】:2018-02-05 09:40:03
【问题描述】:

我有一个使用click 包装为命令的函数。所以它看起来像这样:

@click.command()
@click.option('-w', '--width', type=int, help="Some helping message", default=0)
[... some other options ...]
def app(width, [... some other option arguments...]):
    [... function code...]

这个函数有不同的用例。有时候通过命令行调用就可以了,有时候我也想直接调用函数

from file_name import app
width = 45
app(45, [... other arguments ...]) 

我们怎样才能做到这一点?我们如何使用 click 调用已包装为命令的函数?我找到了这个related post,但我不清楚如何使其适应我的情况(即从头开始构建一个上下文类并在点击命令函数之外使用它)。

编辑:我应该提到:我不能(轻松)修改包含要调用的函数的包。所以我正在寻找的解决方案是如何从调用方处理它。

【问题讨论】:

  • 对我来说还不够清楚你在外部给予了什么。

标签: python command-line-interface python-click


【解决方案1】:

您可以通过从参数重构命令行来从常规代码调用click 命令函数。使用您的示例,它可能看起来像这样:

call_click_command(app, width, [... other arguments ...])

代码:

def call_click_command(cmd, *args, **kwargs):
    """ Wrapper to call a click command

    :param cmd: click cli command function to call 
    :param args: arguments to pass to the function 
    :param kwargs: keywrod arguments to pass to the function 
    :return: None 
    """

    # Get positional arguments from args
    arg_values = {c.name: a for a, c in zip(args, cmd.params)}
    args_needed = {c.name: c for c in cmd.params
                   if c.name not in arg_values}

    # build and check opts list from kwargs
    opts = {a.name: a for a in cmd.params if isinstance(a, click.Option)}
    for name in kwargs:
        if name in opts:
            arg_values[name] = kwargs[name]
        else:
            if name in args_needed:
                arg_values[name] = kwargs[name]
                del args_needed[name]
            else:
                raise click.BadParameter(
                    "Unknown keyword argument '{}'".format(name))


    # check positional arguments list
    for arg in (a for a in cmd.params if isinstance(a, click.Argument)):
        if arg.name not in arg_values:
            raise click.BadParameter("Missing required positional"
                                     "parameter '{}'".format(arg.name))

    # build parameter lists
    opts_list = sum(
        [[o.opts[0], str(arg_values[n])] for n, o in opts.items()], [])
    args_list = [str(v) for n, v in arg_values.items() if n not in opts]

    # call the command
    cmd(opts_list + args_list)

这是如何工作的?

这是可行的,因为 click 是一个设计良好的 OO 框架。 @click.Command 对象可以被内省以确定它期望的参数。然后可以构造一个类似于 click 期望的命令行的命令行。

测试代码:

import click

@click.command()
@click.option('-w', '--width', type=int, default=0)
@click.option('--option2')
@click.argument('argument')
def app(width, option2, argument):
    click.echo("params: {} {} {}".format(width, option2, argument))
    assert width == 3
    assert option2 == '4'
    assert argument == 'arg'


width = 3
option2 = 4
argument = 'arg'

if __name__ == "__main__":
    commands = (
        (width, option2, argument, {}),
        (width, option2, dict(argument=argument)),
        (width, dict(option2=option2, argument=argument)),
        (dict(width=width, option2=option2, argument=argument),),
    )

    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('> {}'.format(cmd))
            time.sleep(0.1)
            call_click_command(app, *cmd[:-1], **cmd[-1])

        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)]
-----------
> (3, 4, 'arg', {})
params: 3 4 arg
-----------
> (3, 4, {'argument': 'arg'})
params: 3 4 arg
-----------
> (3, {'option2': 4, 'argument': 'arg'})
params: 3 4 arg
-----------
> ({'width': 3, 'option2': 4, 'argument': 'arg'},)
params: 3 4 arg

【讨论】:

    【解决方案2】:

    我尝试使用 Python 3.7 并单击 7 以下代码:

    import click
    
    @click.command()
    @click.option('-w', '--width', type=int, default=0)
    @click.option('--option2')
    @click.argument('argument')
    def app(width, option2, argument):
        click.echo("params: {} {} {}".format(width, option2, argument))
        assert width == 3
        assert option2 == '4'
        assert argument == 'arg'
    
    
    app(["arg", "--option2", "4", "-w", 3])
    
    app(["arg", "-w", 3, "--option2", "4" ])
    
    app(["-w", 3, "--option2", "4", "arg"])
    
    

    所有app 调用都正常!

    【讨论】:

    • 我只接到一个电话:params: 3 4 arg 在 py 3.6.8 中,点击 7.0。不方便,不能再调用 app(1,2,3)...
    • @jaromrax 你的意思是在第一个应用程序调用后程序退出?我遇到了这个问题,并通过使用 try except 语句包围每个应用程序调用来解决它。特别是除了 SystemExit
    • 不错。这对我来说是最简单的解决方案。 T
    【解决方案3】:

    如果只想调用底层函数,可以直接访问click.Command.callback。 Click 将底层包装的 Python 函数存储为类成员。请注意,直接调用该函数将绕过所有点击验证,并且不会有任何点击上下文信息。

    这是一个示例代码,它迭代当前 Python 模块中的所有 click.Command 对象,并从中生成可调用函数的字典。

    from functools import partial
    from inspect import getmembers
    
    import click
    
    
    all_functions_of_click_commands = {}
    
    def _call_click_command(cmd: click.Command, *args, **kwargs):
        result = cmd.callback(*args, **kwargs)
        return result
    
    # Pull out all Click commands from the current module
    module = sys.modules[__name__]
    for name, obj in getmembers(module):
        if isinstance(obj, click.Command) and not isinstance(obj, click.Group):
            # Create a wrapper Python function that calls click Command.
            # Click uses dash in command names and dash is not valid Python syntax
            name = name.replace("-", "_") 
            # We also set docstring of this function correctly.
            func = partial(_call_click_command, obj)
            func.__doc__ = obj.__doc__
            all_functions_of_click_commands[name] = func
    

    完整的例子可以在binance-api-test-tool源代码中找到。

    【讨论】:

      【解决方案4】:

      这个用例是described in the docs。

      有时,从另一个命令调用一个命令可能会很有趣。 Click 通常不鼓励这种模式,但仍然可能。为此,您可以使用 Context.invoke() 或 Context.forward() 方法。

      cli = click.Group()
      
      @cli.command()
      @click.option('--count', default=1)
      def test(count):
          click.echo('Count: %d' % count)
      
      @cli.command()
      @click.option('--count', default=1)
      @click.pass_context
      def dist(ctx, count):
          ctx.forward(test)
          ctx.invoke(test, count=42)
      

      它们的工作方式类似,但不同之处在于 Context.invoke() 仅使用您作为调用者提供的参数调用另一个命令,而 Context.forward() 填充来自当前命令的参数。

      【讨论】:

        【解决方案5】:

        这是一个使用选项字典调用点击函数的解决方案:

        有时,从另一个命令调用一个命令可能会很有趣 命令。这是一种通常不鼓励使用 Click 的模式, 但仍有可能。为此,您可以使用 Context.invoke() 或 Context.forward() 方法。

        它们的工作方式类似,但不同之处在于 Context.invoke() 仅使用您提供的参数调用另一个命令 调用者,而 Context.forward() 填充来自 当前命令。两者都接受命令作为第一个参数,并且 其他所有内容都会按照您的预期继续传递。

        例子:

        cli = click.Group()
        
        @cli.command()
        @click.option('--opt1', default=1)
        @click.option('--opt2', default=2)
        def test(opt1, opt2):
            print(opt1)
            print(opt2)
        
        @cli.command()
        @click.pass_context
        def dist(ctx):
            args = {"opt1":3, "opt2": 4}
            ctx.invoke(test, **args)
        
        
        if __name__ == "__main__":
            dist()
        

        【讨论】:

          猜你喜欢
          • 2012-04-24
          • 2017-02-26
          • 2014-06-02
          • 1970-01-01
          • 2011-12-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多