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


82

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

这是一个我想如何分开的例子:

在里面

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

Answers:


91

使用CommandCollection此功能的缺点是它会合并您的命令,并且仅适用于命令组。imho更好的替代方法是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类和模块的全部功能。

Mycli.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


如果它们在单独的模块中,如何将上下文传递给子命令?
2015年

2
@vishal,请查看文档的这一部分:click.pocoo.org/6/commands/#nested-handling-and-contexts您可以使用decorator将上下文对象传递给任何命令@click.pass_context。或者,也有一个称为全局上下文访问的东西:click.pocoo.org/6/advanced/#global-context-access
jdno

6
我使用@jdno指南编译了MWE。您可以在这里
Dror

我怎样才能放平所有组命令?我的意思是所有第一级命令。
秘银

3
@Mithril使用一个CommandCollection。奥斯卡的答案举了一个例子,在click的文档中有一个非常不错的例子:click.palletsprojects.com/en/7.x/commands/…
jdno

34

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

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

组无非就是多个命令,并且可以嵌套组。您可以将组分为模块,然后将其导入init.py文件中,然后cli使用add_command将它们添加到组中。

这是一个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

该信息在文档中不是很明确,但是如果您查看源代码,并对其进行了很好的注释,则可以看到如何嵌套组。


5
同意。数量很少,因此它应该是文档的一部分。正是我要构建复杂工具所需要的!谢谢🙏!
西蒙·肯珀

确实不错,但有一个问题:考虑您的示例,如果从其他地方导入,应该@cloudflare.command()zone函数中删除吗?zone
埃丁·埃里

这是我一直在寻找的出色信息。如何指挥组区分另一个很好的例子可以在这里找到:github.com/dagster-io/dagster/tree/master/python_modules/...
托马斯克林格

10

我目前正在寻找这样的事情,就您而言,这很简单,因为每个文件中都有组,您可以按照文档中的说明解决此问题:

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,因为您不需要导入不需要的东西,也不需要从任何地方导入*。


您能告诉我什么要放在子命令文件中吗?我必须cli从init.py导入main ,但这会导致循环导入。你能解释一下怎么做吗?
grundic

@grundic如果您还没有找到解决方案,请查看我的答案。这可能会使您走上正确的道路。
jdno

1
@grundic我希望您已经知道了,但是在您的子命令文件中,您只创建了一个新的文件click.group,该文件是您在顶级CLI中导入的文件。
奥斯卡·戴维·阿贝莱兹

5

我花了一些时间才弄清楚这一点,但我想当我忘记如何做时,我会把它放在这里以提醒自己,我认为部分问题是在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编译”应该给我们

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,而且我在其中包含了一条主语句


0

我不是一位点击专家,但只要将文件导入到主文件中,它就可以工作。我将所有命令移到单独的文件中,并让一个主文件导入其他文件。这样一来,如果对您来说很重要,就可以更轻松地控制确切的订单。因此您的主文件如下所示:

import commands_main
import commands_cloudflare
import commands_uptimerobot

0

编辑:刚刚意识到我的答案/评论只不过是对Click的官方文档在“ Custom Multi Commands”部分中提供的内容的重新混搭:https : //click.palletsprojects.com/en/7.x/commands/#custom -多命令

只是为了添加@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”包围的每个文件路径,然后执行import和add命令。

这是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)

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.