【问题标题】:Call another click command from a click command从单击命令调用另一个单击命令
【发布时间】:2017-02-26 17:31:18
【问题描述】:

我想使用一些有用的功能作为命令。为此,我正在测试click 库。我定义了我的三个原始函数,然后装饰为click.command

import click
import os, sys

@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=True)
def add_name(content, to_stdout=False):
    if not content:
        content = ''.join(sys.stdin.readlines())
    result = content + "\n\tadded name"
    if to_stdout is True:
        sys.stdout.writelines(result)
    return result


@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=True)
def add_surname(content, to_stdout=False):
    if not content:
        content = ''.join(sys.stdin.readlines())
    result = content + "\n\tadded surname"
    if to_stdout is True:
        sys.stdout.writelines(result)
    return result

@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=False)
def add_name_and_surname(content, to_stdout=False):
    result = add_surname(add_name(content))
    if to_stdout is True:
        sys.stdout.writelines(result)
    return result

通过这种方式,我可以使用setup.py 文件和pip install --editable . 生成三个命令add_nameadd_surnameadd_name_and_surname,然后我就可以进行管道传输了:

$ echo "original content" | add_name | add_surname 
original content

    added name
    added surname

但是,当使用不同的点击命令作为函数编写时,我需要解决一个小问题:

$echo "original content" | add_name_and_surname 
Usage: add_name_and_surname [OPTIONS] [CONTENT]

Error: Got unexpected extra arguments (r i g i n a l   c o n t e n t 
)

我不知道为什么它不起作用,我需要这个 add_name_and_surname 命令来调用 add_nameadd_surname 不是作为命令而是作为函数,否则它违背了我使用函数作为库函数和命令。

【问题讨论】:

标签: python command-line-arguments stdout piping python-click


【解决方案1】:

当您直接从另一个函数调用 add_name()add_surname() 时,您实际上调用了它们的修饰版本,因此预期的参数可能与您定义的不同(有关原因的一些详细信息,请参阅How to strip decorators from a function in python 的答案)。

我建议您修改您的实现,以便您保持原始函数未修饰并为它们创建瘦点击特定包装器,例如:

def add_name(content, to_stdout=False):
    if not content:
        content = ''.join(sys.stdin.readlines())
    result = content + "\n\tadded name"
    if to_stdout is True:
        sys.stdout.writelines(result)
    return result

@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=True)
def add_name_command(content, to_stdout=False):
    return add_name(content, to_stdout)

然后,您可以直接调用这些函数,也可以通过 setup.py 创建的 CLI 包装脚本调用它们。

这可能看起来多余,但实际上可能是正确的方法:一个函数代表您的业务逻辑,另一个(点击命令)是通过命令行公开此逻辑的“控制器”(可能有,例如例如,还有一个通过 Web 服务公开相同逻辑的函数)。

事实上,我什至建议将它们放在单独的 Python 模块中 - 您的“核心”逻辑和特定于点击的实现,如果需要,可以替换为任何其他界面。

【讨论】:

  • 关于传递点击上下文的答案似乎更适合这个问题。然而,这和剥离装饰器都被认为是脏的,不建议使用。最后,基于控制器的替代方案更加“通用”和可靠。我会走这条路,非常感谢!
  • +1 到这个,因为 Click 不鼓励从另一个命令调用一个命令。这种与业务逻辑和 cli 控制器包装器恕我直言的分离是有道理的。
  • 如果您希望 CLI 版本与“核心”函数具有相同的名称,请像上面的 @click.command(name='add_name') 一样传递 name 参数。
  • 恕我直言,这在所有情况下都是一个更好的答案,并且在所有情况下您都会得到更清晰、更清晰的代码。
【解决方案2】:

由于点击装饰器,函数不能再仅通过指定参数来调用。 Context 类是你的朋友,具体来说:

  1. Context.invoke() - 使用您提供的参数调用另一个命令
  2. Context.forward() - 填充当前命令的参数

所以您的 add_name_and_surname 代码应如下所示:

@click.command()
@click.argument('content', required=False)
@click.option('--to_stdout', default=False)
@click.pass_context
def add_name_and_surname(ctx, content, to_stdout=False):
    result = ctx.invoke(add_surname, content=ctx.forward(add_name))
    if to_stdout is True:
        sys.stdout.writelines(result)
    return result

参考: http://click.pocoo.org/6/advanced/#invoking-other-commands

【讨论】:

【解决方案3】:

我发现这些解决方案更加复杂。我希望从另一个包的另一个地方调用下面的这个函数:

@click.command(help='Clean up')
@click.argument('path', nargs=1, default='def')
@click.option('--info', '-i', is_flag=True,
              help='some info1')
@click.option('--total', '-t', is_flag=True,
              help='some info2')
def clean(path, info, total):
#some definition, some actions

#this function will help us
def get_function(function_name):
if function_name == 'clean':
    return clean

我有另一个包,所以,我想在这个包中点击上面的命令

import somepackage1 #here is clean up click command
from click.testing import CliRunner


@check.command(context_settings=dict(
           ignore_unknown_options=True,
          ))
@click.argument('args', nargs=-1)
@click.pass_context
def check(ctx, args):
    runner = CliRunner()

    if len(args[0]) == 0:
        logger.error('Put name of a command')

    if len(args) > 0:
        result = runner.invoke(somepackage1.get_function(args[0]), args[1:])
        logger.print(result.output)
    else:
        result = runner.invoke(somepackage1.get_function(args[0]))
    logger.print(result.output)

所以,它有效。

python somepackage2 check clean params1 --info

【讨论】:

    【解决方案4】:

    在某些情况下,您还可以使用callback 成员函数调用 Click 函数。根据this GitHub issue

    假设您知道给定命令是您编写的函数的直接包装器(而不是组或其他类型的命令),您可以使用 command.callback 获取该函数。但是,调用它只会调用函数,它不会调用任何 Click 管道进行验证、回调等。

    你的例子将会变成:

    (...)
    def add_name_and_surname(content, to_stdout=False):
        result = add_surname.callback(add_name.callback(content))
        (...)
    

    【讨论】: