Làm cách nào tôi có thể chia các lệnh Click của mình, mỗi lệnh có một tập hợp các lệnh con, thành nhiều tệp?


82

Tôi có một ứng dụng nhấp chuột lớn mà tôi đã phát triển, nhưng việc điều hướng qua các lệnh / lệnh con khác nhau đang trở nên khó khăn. Làm cách nào để tổ chức các lệnh của tôi thành các tệp riêng biệt? Có thể tổ chức các lệnh và lệnh con của chúng thành các lớp riêng biệt không?

Đây là một ví dụ về cách tôi muốn tách nó ra:

trong đó

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

Câu trả lời:


91

Nhược điểm của việc sử dụng CommandCollectioncho điều này là nó hợp nhất các lệnh của bạn và chỉ hoạt động với các nhóm lệnh. Sự thay thế tốt hơn imho là sử dụng add_commandđể đạt được kết quả tương tự.

Tôi có một dự án với cây sau:

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

Mỗi lệnh con có mô-đun riêng của nó, điều này làm cho nó vô cùng dễ dàng quản lý ngay cả các triển khai phức tạp với nhiều lớp và tệp trợ giúp hơn. Trong mỗi mô-đun, commands.pytệp chứa các @clickchú thích. Ví dụ group2/commands.py:

import click


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

Nếu cần, bạn có thể dễ dàng tạo thêm các lớp trong mô-đun importvà sử dụng chúng tại đây, do đó cung cấp cho CLI của bạn toàn bộ sức mạnh của các lớp và mô-đun của Python.

Của tôi cli.pylà điểm đầu vào cho toàn bộ 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)

Với thiết lập này, rất dễ dàng phân tách các lệnh của bạn theo các mối quan tâm và cũng xây dựng chức năng bổ sung xung quanh chúng mà chúng có thể cần. Nó đã phục vụ tôi rất tốt cho đến nay ...

Tham khảo: http://click.pocoo.org/6/quickstart/#nesting-commands


làm thế nào để chuyển ngữ cảnh cho lệnh con nếu chúng nằm trong các mô-đun riêng biệt?
vishal

2
@vishal, hãy xem phần này của tài liệu: click.pocoo.org/6/commands/#nested-handling-and-contexts Bạn có thể chuyển đối tượng ngữ cảnh vào bất kỳ lệnh nào bằng trình trang trí @click.pass_context. Ngoài ra, còn có một thứ gọi là Truy cập ngữ cảnh toàn cầu : click.pocoo.org/6/advanced/#global-context-access .
jdno

6
Tôi đã biên soạn một MWE bằng cách sử dụng hướng dẫn @jdno. Bạn có thể tìm thấy nó ở đây
Dror

Làm cách nào tôi có thể làm phẳng tất cả lệnh nhóm? Ý tôi là, tất cả các lệnh ở cấp độ đầu tiên.
Mithril

3
@Mithril Sử dụng a CommandCollection. Câu trả lời của Oscar có một ví dụ, và có một ví dụ rất hay trong tài liệu hướng dẫn nhấp chuột: click.palletsprojects.com/en/7.x/commands/… .
jdno

34

Giả sử dự án của bạn có cấu trúc sau:

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

Nhóm chỉ là nhiều lệnh và các nhóm có thể được lồng vào nhau. Bạn có thể tách các nhóm của mình thành các mô-đun và nhập chúng vào init.pytệp của bạn và thêm chúng vào clinhóm bằng cách sử dụng lệnh add_command.

Đây là một init.pyví dụ:

import click
from .commands.cloudflare import cloudflare


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


cli.add_command(cloudflare)

Bạn phải nhập nhóm cloudflare nằm bên trong tệp cloudflare.py. Của bạn commands/cloudflare.pysẽ trông như thế này:

import click


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


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

Sau đó, bạn có thể chạy lệnh cloudflare như sau:

$ python init.py cloudflare zone

Thông tin này không rõ ràng trên tài liệu nhưng nếu bạn nhìn vào mã nguồn, được nhận xét rất tốt, bạn có thể thấy cách các nhóm có thể được lồng vào nhau.


5
Đồng ý. Tối thiểu đến mức nó phải là một phần của tài liệu. Chính xác những gì tôi đang tìm kiếm để xây dựng các công cụ phức tạp! Cảm ơn 🙏!
Simon Kemper

Nó chắc chắn là tuyệt vời nhưng có một câu hỏi: Xem xét ví dụ của bạn, tôi có nên xóa @cloudflare.command()khỏi zonechức năng nếu tôi nhập zonetừ một nơi khác không?
Erdin Eray

Đây là một thông tin tuyệt vời mà tôi đang tìm kiếm. Một ví dụ điển hình khác về cách phân biệt giữa các nhóm lệnh có thể được tìm thấy tại đây: github.com/dagster-io/dagster/tree/master/python_modules/…
Thomas Klinger

10

Tôi đang tìm một cái gì đó như thế này vào lúc này, trong trường hợp của bạn rất đơn giản vì bạn có các nhóm trong mỗi tệp, bạn có thể giải quyết vấn đề này như được giải thích trong tài liệu :

Trong init.pytệp:

import click

from command_cloudflare import cloudflare
from command_uptimerobot import uptimerobot

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

if __name__ == '__main__':
    cli()

Phần tốt nhất của giải pháp này là hoàn toàn tương thích với pep8 và các linters khác vì bạn không cần nhập thứ gì đó mà bạn sẽ không sử dụng và bạn không cần nhập * từ bất kỳ đâu.


Bạn có thể vui lòng cho biết, những gì để đưa vào các tệp lệnh con? Tôi phải nhập chính clitừ init.py, nhưng điều này dẫn đến nhập vòng tròn. Bạn có thể vui lòng giải thích làm thế nào để làm điều đó?
grundic

@grundic Hãy xem câu trả lời của tôi nếu bạn chưa tìm ra giải pháp. Nó có thể đưa bạn đi đúng hướng.
jdno

1
@grundic Tôi hy vọng bạn đã tìm ra, nhưng trong các tệp lệnh phụ của bạn, bạn chỉ cần tạo một tệp mới click.groupmà bạn nhập trong CLI cấp cao nhất.
Oscar David Arbeláez

5

Tôi đã mất một lúc để tìm ra điều này nhưng tôi nghĩ rằng tôi sẽ đặt điều này ở đây để nhắc nhở bản thân khi tôi quên cách làm lại. Tôi nghĩ một phần của vấn đề là hàm add_command được đề cập trên trang github của nhấp chuột nhưng không phải là chính trang ví dụ

trước tiên hãy tạo một tệp python ban đầu có tên 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()

Tiếp theo, hãy đặt một số lệnh công cụ vào một tệp có tên 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()

Tiếp theo, hãy đặt một số lệnh biên dịch trong một tệp có tên 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()

chạy root.py bây giờ sẽ cung cấp cho chúng tôi

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

  Demo

Options:
  --help  Show this message and exit.

Commands:
  compile  Commands related to compiling
  tools    Tool related commands

chạy "root.py biên dịch" sẽ cung cấp cho chúng tôi

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

Bạn cũng sẽ nhận thấy rằng bạn có thể chạy cli_tools.py hoặc cli_compile.py trực tiếp cũng như tôi đã bao gồm một tuyên bố chính trong đó


0

Tôi không phải là chuyên gia về nhấp chuột, nhưng nó sẽ hoạt động bằng cách chỉ nhập các tệp của bạn vào tệp chính. Tôi sẽ di chuyển tất cả các lệnh trong các tệp riêng biệt và có một tệp chính nhập các tệp khác. Bằng cách đó, sẽ dễ dàng hơn để kiểm soát thứ tự chính xác, trong trường hợp nó quan trọng đối với bạn. Vì vậy, tệp chính của bạn sẽ giống như sau:

import commands_main
import commands_cloudflare
import commands_uptimerobot

0

chỉnh sửa: chỉ cần nhận ra rằng câu trả lời / nhận xét của tôi chỉ là một bản rehash những gì tài liệu chính thức của Click cung cấp trong phần "Tùy chỉnh nhiều lệnh": https://click.palletsprojects.com/en/7.x/commands/#custom -multi-lệnh

Chỉ để thêm vào câu trả lời tuyệt vời, được chấp nhận bởi @jdno, tôi đã nghĩ ra một chức năng trợ giúp tự động nhập và tự động thêm các mô-đun lệnh con, điều này cắt giảm đáng kể bảng soạn sẵn trong cli.py:

Cấu trúc dự án của tôi là:

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

Mỗi tệp lệnh con trông giống như sau:

import click

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

(hiện tại, tôi chỉ có một lệnh con cho mỗi tệp)

Trong cli.py, tôi đã viết mộtadd_subcommand() hàm lặp qua mọi đường dẫn tệp được phân chia bởi "subcommands / *. Py" và sau đó thực hiện lệnh nhập và thêm.

Đây là phần nội dung của tập lệnh cli.py được đơn giản hóa thành:

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

Và đây là những gì add_subcommands() hàm trông như thế này:


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)

Tôi không biết điều này mạnh đến mức nào nếu tôi thiết kế một lệnh có nhiều cấp độ lồng ghép và chuyển đổi ngữ cảnh. Nhưng nó có vẻ hoạt động tốt cho bây giờ :)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.