각각의 하위 명령 세트가있는 Click 명령을 여러 파일로 분할하려면 어떻게해야합니까?


83

개발 한 큰 클릭 응용 프로그램이 하나 있지만 다른 명령 / 하위 명령을 탐색하는 것이 어려워지고 있습니다. 명령을 별도의 파일로 구성하려면 어떻게합니까? 명령과 하위 명령을 별도의 클래스로 구성 할 수 있습니까?

다음은이를 분리하는 방법의 예입니다.

초기화

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

답변:


92

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의 클래스 및 모듈을 최대한 활용할 수 있습니다.

My 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

2
@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

6
@jdno 지침을 사용하여 MWE를 컴파일했습니다. 여기에서
Dror

모든 그룹 명령을 어떻게 평면화 할 수 있습니까? 내 말은, 첫 번째 수준의 모든 명령입니다.
Mithril

3
@Mithril CommandCollection. Oscar의 답변에는 예가 있으며 click 문서에는 click.palletsprojects.com/en/7.x/commands/… 정말 멋진 답변이 있습니다.
jdno

34

프로젝트의 구조가 다음과 같다고 가정합니다.

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

그룹은 여러 명령에 지나지 않으며 그룹은 중첩 될 수 있습니다. 그룹을 모듈로 분리하고 init.py파일로 가져 와서 cliadd_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
동의하다. 문서의 일부가되어야 할만큼 최소한입니다. 복잡한 도구를 만들기 위해 내가 찾던 바로 그것! 감사합니다 🙏!
Simon Kemper

확실히 훌륭하지만 질문이 있습니다. 예를 고려할 때 다른 곳에서 가져 오면 함수 @cloudflare.command()에서 제거해야 합니까? zonezone
Erdin Eray

이것은 제가 찾고 있던 훌륭한 정보입니다. 명령 그룹을 구별하는 방법에 대한 또 다른 좋은 예는 다음에서 찾을 수 있습니다. github.com/dagster-io/dagster/tree/master/python_modules/…
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 및 기타 linters와 완전히 호환된다는 것입니다.


하위 명령 파일에 무엇을 넣을지 말씀해 주시겠습니까? cliinit.py에서 main 을 가져와야하지만 이것은 순환 가져 오기로 이어집니다. 어떻게하는지 설명해 주시겠습니까?
grundic

@grundic 아직 해결책을 찾지 못했다면 내 대답을 확인하십시오. 그것은 당신을 올바른 길로 인도 할 수 있습니다.
jdno

1
@grundic 이미 생각 했길 바라지 만 하위 명령 파일 click.group에서 최상위 CLI에서 가져온 새 파일을 만듭니다 .
Oscar David Arbeláez

5

이 문제를 해결하는 데 시간이 좀 걸렸지 만 다시 수행하는 방법을 잊었을 때이를 상기시키기 위해 여기에 넣어야한다고 생각했습니다. 문제의 일부는 add_command 함수가 클릭의 github 페이지에서 언급되었지만 메인이 아니라는 것입니다. 예제 페이지

먼저 root.py라는 초기 파이썬 파일을 만들 수 있습니다.

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")

(지금은 파일 당 하나의 하위 명령 만 있습니다)

에서는 "subcommands / *. py"에 의해 globbed 된 모든 파일 경로를 반복 cli.py하는 add_subcommand()함수를 작성 하고 가져 오기 및 추가 명령을 수행합니다.

다음은 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.