【问题标题】:How can I split my Click commands, each with a set of sub-commands, into multiple files?如何将每个带有一组子命令的 Click 命令拆分为多个文件?
【发布时间】: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

【问题讨论】:

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


    【解决方案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

    【讨论】:

    【解决方案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 文件中运行。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-03-16
          • 1970-01-01
          • 1970-01-01
          • 2016-05-20
          • 1970-01-01
          • 1970-01-01
          • 2021-01-13
          • 1970-01-01
          相关资源
          最近更新 更多