それぞれがサブコマンドのセットを持つClickコマンドを複数のファイルに分割するにはどうすればよいですか?


82

私が開発した1つの大きなクリックアプリケーションがありますが、さまざまなコマンド/サブコマンドをナビゲートするのが難しくなっています。コマンドを個別のファイルに整理するにはどうすればよいですか?コマンドとそのサブコマンドを別々のクラスに編成することは可能ですか?

これが私がそれをどのように分離したいかの例です:

初期化

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

回答:


91

これを使用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 2017年

2
@vishal、ドキュメントの次のセクションをご覧ください。click.pocoo.org / 6 / commands / #nested-handling-and-contextsデコレータを使用して任意のコマンドにコンテキストオブジェクトを渡すことができます@click.pass_context。または、グローバルコンテキストアクセスと呼ばれるものもあります:click.pocoo.org/6/advanced/#global-context-access
jdno 2017年

6
@jdnoガイドラインを使用してMWEをコンパイルしました。あなたはここで
Dror 2017

すべてのグループコマンドをフラットにするにはどうすればよいですか?つまり、最初のレベルのすべてのコマンド。
ミスリル

3
@Mithrilを使用しCommandCollectionます。オスカーの答えには例があり、クリックのドキュメントには本当に素晴らしいものがあります: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
同意します。最小限なので、ドキュメントの一部にする必要があります。まさに私が複雑なツールを構築するために探していたものです!ありがとう🙏!
サイモンケンパー2018

それは確かに素晴らしいですが、質問があります:あなたの例を考えると、他の場所からインポートする場合@cloudflare.command()zone関数から削除する必要がありますか?zone
ErdinEray19年

これは私が探していた優れた情報です。コマンドグループを区別する方法のもう1つの良い例は、次の場所にあります。github.com
Thomas Klinger

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やその他のリンターに完全に準拠していることです。


サブコマンドファイルに何を入れるか教えていただけますか?cliinit.pyからmainをインポートする必要がありますが、これは循環インポートにつながります。その方法を教えてください。
grundic 2016

@grundicまだ解決策がわからない場合は、私の答えを確認してください。それはあなたを正しい軌道に乗せるかもしれません。
jdno 2016

1
@grundicすでに理解しているといいのですが、サブコマンドファイルclick.groupで、トップレベルのCLIにインポートする新しいファイルを作成するだけです。
オスカーデビッドアルベラエス2016年

5

これを理解するのに少し時間がかかりましたが、私はこれをここに置いて、もう一度行う方法を忘れたときに思い出させると思いました。問題の一部は、add_command関数がクリックのgithubページに記載されているが、メインではないことだと思います例のページ

まず、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.pycompile」を実行すると、

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

私はクリックの専門家ではありませんが、ファイルをメインのファイルにインポートするだけで機能するはずです。すべてのコマンドを別々のファイルに移動し、1つのメインファイルに他のファイルをインポートさせます。そうすれば、それがあなたにとって重要である場合に備えて、正確な順序を制御することがより簡単になります。したがって、メインファイルは次のようになります。

import commands_main
import commands_cloudflare
import commands_uptimerobot

0

編集:私の回答/コメントは、「カスタムマルチコマンド」セクションでClickの公式ドキュメントが提供するものの再ハッシュにすぎないことに気づきました: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")

(今のところ、ファイルごとに1つのサブコマンドしかありません)

cli.pyadd_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)

いくつかのレベルのネストとコンテキスト切り替えを備えたコマンドを設計した場合、これがどれほど堅牢かはわかりません。しかし、今のところは問題なく動作しているようです:)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.