如何将每个带有一组子命令的 Click 命令拆分为多个文件?

Posted

技术标签:

【中文标题】如何将每个带有一组子命令的 Click 命令拆分为多个文件?【英文标题】:How can I split my Click commands, each with a set of sub-commands, into multiple files? 【发布时间】:2016-04-11 03:54:30 【问题描述】:

我已经开发了一个大型点击应用程序,但浏览不同的命令/子命令变得很困难。如何将我的命令组织到单独的文件中?是否可以将命令及其子命令组织到单独的类中?

这是我想如何将其分开的示例:

初始化

import click

@click.group()
@click.version_option()
def cli():
    pass #Entry Point

command_cloudflare.py

​​>
@cli.group()
@click.pass_context
def cloudflare(ctx):
    pass

@cloudflare.group('zone')
def cloudflare_zone():
    pass

@cloudflare_zone.command('add')
@click.option('--jumpstart', '-j', default=True)
@click.option('--organization', '-o', default='')
@click.argument('url')
@click.pass_obj
@__cf_error_handler
def cloudflare_zone_add(ctx, url, jumpstart, organization):
    pass

@cloudflare.group('record')
def cloudflare_record():
    pass

@cloudflare_record.command('add')
@click.option('--ttl', '-t')
@click.argument('domain')
@click.argument('name')
@click.argument('type')
@click.argument('content')
@click.pass_obj
@__cf_error_handler
def cloudflare_record_add(ctx, domain, name, type, content, ttl):
    pass

@cloudflare_record.command('edit')
@click.option('--ttl', '-t')
@click.argument('domain')
@click.argument('name')
@click.argument('type')
@click.argument('content')
@click.pass_obj
@__cf_error_handler
def cloudflare_record_edit(ctx, domain):
    pass

command_uptimerobot.py

​​>
@cli.group()
@click.pass_context
def uptimerobot(ctx):
    pass

@uptimerobot.command('add')
@click.option('--alert', '-a', default=True)
@click.argument('name')
@click.argument('url')
@click.pass_obj
def uptimerobot_add(ctx, name, url, alert):
    pass

@uptimerobot.command('delete')
@click.argument('names', nargs=-1, required=True)
@click.pass_obj
def uptimerobot_delete(ctx, names):
    pass

【问题讨论】:

【参考方案1】:

为此使用CommandCollection 的缺点是它会合并您的命令并且仅适用于命令组。恕我直言,更好的选择是使用add_command 来实现相同的结果。

我有一个包含以下树的项目:

cli/
├── __init__.py
├── cli.py
├── group1
│   ├── __init__.py
│   ├── commands.py
└── group2
    ├── __init__.py
    └── commands.py

每个子命令都有自己的模块,这使得管理具有更多帮助类和文件的复杂实现变得异常容易。在每个模块中,commands.py 文件包含@click 注释。示例group2/commands.py

import click


@click.command()
def version():
    """Display the current version."""
    click.echo(_read_version())

如有必要,您可以轻松地在模块中创建更多类,import 并在此处使用它们,从而为您的 CLI 提供 Python 类和模块的全部功能。

我的cli.py 是整个 CLI 的入口点:

import click

from .group1 import commands as group1
from .group2 import commands as group2

@click.group()
def entry_point():
    pass

entry_point.add_command(group1.command_group)
entry_point.add_command(group2.version)

使用此设置,可以很容易地按关注点分隔命令,并围绕它们构建可能需要的附加功能。到目前为止,它对我很有帮助...

参考: http://click.pocoo.org/6/quickstart/#nesting-commands

【讨论】:

如果它们位于不同的模块中,如何将上下文传递给子命令? @vishal,查看文档的这一部分:click.pocoo.org/6/commands/#nested-handling-and-contexts 您可以使用装饰器 @click.pass_context 将上下文对象传递给任何命令。或者,还有一个叫做 Global Context Access 的东西:click.pocoo.org/6/advanced/#global-context-access. 我使用@jdno 指南编译了一个 MWE。你可以找到它here 我怎样才能扁平化所有组命令?我的意思是,第一级的所有命令。 @Mithril 使用CommandCollection。 Oscar 的答案有一个例子,click 的文档中有一个非常好的例子:click.palletsprojects.com/en/7.x/commands/…。【参考方案2】:

假设您的项目具有以下结构:

project/
├── __init__.py
├── init.py
└── commands
    ├── __init__.py
    └── cloudflare.py

组只不过是多个命令,组可以嵌套。您可以将您的组分成模块并将它们导入到您的 init.py 文件中,然后使用 add_command 将它们添加到 cli 组中。

这是一个init.py 示例:

import click
from .commands.cloudflare import cloudflare


@click.group()
def cli():
    pass


cli.add_command(cloudflare)

您必须导入位于 cloudflare.py 文件中的 cloudflare 组。你的commands/cloudflare.py 看起来像这样:

import click


@click.group()
def cloudflare():
    pass


@cloudflare.command()
def zone():
    click.echo('This is the zone subcommand of the cloudflare command')

然后你可以像这样运行 cloudflare 命令:

$ python init.py cloudflare zone

此信息在文档中不是很明确,但如果您查看源代码,其中有很好的注释,您可以看到组是如何嵌套的。

【讨论】:

同意。如此之少,以至于它应该成为文档的一部分。正是我正在寻找构建复杂工具的东西!谢谢?! 这确实很棒,但有一个问题:考虑到您的示例,我是否应该从zone 函数中删除@cloudflare.command() 如果我从其他地方导入zone 这是我一直在寻找的极好的信息。另一个关于如何区分命令组的好例子可以在这里找到:github.com/dagster-io/dagster/tree/master/python_modules/…【参考方案3】:

我目前正在寻找类似的东西,在您的情况下很简单,因为您在每个文件中都有组,您可以按照documentation 中的说明解决此问题:

init.py 文件中:

import click

from command_cloudflare import cloudflare
from command_uptimerobot import uptimerobot

cli = click.CommandCollection(sources=[cloudflare, uptimerobot])

if __name__ == '__main__':
    cli()

这个解决方案最好的部分是它完全符合 pep8 和其他 linter,因为您不需要导入您不会使用的东西,也不需要从任何地方导入 *。

【讨论】:

你能告诉我在子命令文件里放什么吗?我必须从 init.py 导入 main cli,但这会导致循环导入。你能解释一下怎么做吗? @grundic 如果您还没有找到解决方案,请查看我的答案。它可能会让你走上正轨。 @grundic 我希望你已经想到了,但是在你的子命令文件中你只需要创建一个新的click.group 这是你在*** CLI 中导入的那个。【参考方案4】:

我花了一段时间才弄明白 但我想我会把这个放在这里以提醒自己当我再次忘记该怎么做时 我认为部分问题是在 click 的 github 页面上提到了 add_command 函数,但在主示例页面上没有提到

首先让我们创建一个名为 root.py 的初始 python 文件

import click
from cli_compile import cli_compile
from cli_tools import cli_tools

@click.group()
def main():
    """Demo"""

if __name__ == '__main__':
    main.add_command(cli_tools)
    main.add_command(cli_compile)
    main()

接下来让我们将一些工具命令放在一个名为 cli_tools.py 的文件中

import click

# Command Group
@click.group(name='tools')
def cli_tools():
    """Tool related commands"""
    pass

@cli_tools.command(name='install', help='test install')
@click.option('--test1', default='1', help='test option')
def install_cmd(test1):
    click.echo('Hello world')

@cli_tools.command(name='search', help='test search')
@click.option('--test1', default='1', help='test option')
def search_cmd(test1):
    click.echo('Hello world')

if __name__ == '__main__':
    cli_tools()

接下来让我们将一些编译命令放在一个名为 cli_compile.py 的文件中

import click

@click.group(name='compile')
def cli_compile():
    """Commands related to compiling"""
    pass

@cli_compile.command(name='install2', help='test install')
def install2_cmd():
    click.echo('Hello world')

@cli_compile.command(name='search2', help='test search')
def search2_cmd():
    click.echo('Hello world')

if __name__ == '__main__':
    cli_compile()

运行 root.py 现在应该给我们

Usage: root.py [OPTIONS] COMMAND [ARGS]...

  Demo

Options:
  --help  Show this message and exit.

Commands:
  compile  Commands related to compiling
  tools    Tool related commands

运行“root.py compile”应该给我们

Usage: root.py compile [OPTIONS] COMMAND [ARGS]...

  Commands related to compiling

Options:
  --help  Show this message and exit.

Commands:
  install2  test install
  search2   test search

您还会注意到您可以直接运行 cli_tools.py 或 cli_compile.py 以及我在其中包含一个 main 语句

【讨论】:

如果你的函数被拆分成不同的模块,这是否有效? 我在不同的模块中划分了选项,想法是您可以在一个模块中拥有***菜单,然后在其他模块中拥有更多子选项。【参考方案5】:

编辑: 刚刚意识到我的回答/评论只不过是对 Click 的官方文档在“自定义多命令”部分中提供的内容的重述:https://click.palletsprojects.com/en/7.x/commands/#custom-multi-commands

为了补充@jdno 的优秀、公认的答案,我想出了一个辅助函数,它可以自动导入和自动添加子命令模块,这大大减少了我的cli.py 中的样板:

我的项目结构是这样的:

projectroot/
    __init__.py
    console/
    │
    ├── cli.py
    └── subcommands
       ├── bar.py
       ├── foo.py
       └── hello.py

每个子命令文件如下所示:

import click

@click.command()
def foo():
    """foo this is for foos!"""
    click.secho("FOO", fg="red", bg="white")

(目前,我每个文件只有一个子命令)

cli.py 中,我编写了一个add_subcommand() 函数,该函数循环遍历由“subcommands/*.py”覆盖的每个文件路径,然后执行导入和添加命令。

下面是 cli.py 脚本的主体被简化为:

import click
import importlib
from pathlib import Path
import re

@click.group()
def entry_point():
    """whats up, this is the main function"""
    pass

def main():
    add_subcommands()
    entry_point()

if __name__ == '__main__':
    main()

这就是add_subcommands() 函数的样子:


SUBCOMMAND_DIR = Path("projectroot/console/subcommands")

def add_subcommands(maincommand=entry_point):
    for modpath in SUBCOMMAND_DIR.glob('*.py'):
        modname = re.sub(f'/', '.',  str(modpath)).rpartition('.py')[0]
        mod = importlib.import_module(modname)
        # filter out any things that aren't a click Command
        for attr in dir(mod):
            foo = getattr(mod, attr)
            if callable(foo) and type(foo) is click.core.Command:
                maincommand.add_command(foo)

如果我要设计一个具有多个嵌套和上下文切换级别的命令,我不知道这有多强大。但它现在似乎可以正常工作:)

【讨论】:

【参考方案6】:

我不是点击专家,但只需将文件导入主文件即可。我会将所有命令移动到单独的文件中,并让一个主文件导入其他文件。这样更容易控制确切的顺序,以防它对您很重要。所以你的主文件看起来像:

import commands_main
import commands_cloudflare
import commands_uptimerobot

【讨论】:

【参考方案7】:

当您希望您的用户 pip install "your_module",然后使用命令时,您可以将它们添加到 setup.py entry_points 作为列表:

entry_points=
    'console_scripts': [
        'command_1 = src.cli:function_command_1',
        'command_2 = src.cli:function_command_2',
    ]

每个命令都必须在 cli 文件中运行。

【讨论】:

以上是关于如何将每个带有一组子命令的 Click 命令拆分为多个文件?的主要内容,如果未能解决你的问题,请参考以下文章

如何将带有字典列表的熊猫列拆分为每个键的单独列

如何填充一组子`ref`ed文档猫鼬?

qt - 如何制作带有一组按钮的拆分器?

一组子图的标题

如何将连续变量拆分为等长(定义数量)的区间并仅在 R 中列出带有切点的区间?

在一组子组件中实现 useContext