Як я можу розділити свої команди Click, кожна з набором підкоманд, на кілька файлів?


87

У мене є одна велика програма для клацання, яку я розробив, але навігація по різних командах / підкомандах стає важкою. Як організувати свої команди в окремі файли? Чи можна організувати команди та їх підкоманди в окремі класи?

Ось приклад того, як я хотів би розділити його:

у цьому

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

Відповіді:


99

Недоліком використання 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
@vishal, погляньте на цей розділ документації: click.pocoo.org/6/commands/#nested-handling-and-contexts Ви можете передати контекстний об'єкт будь-якій команді за допомогою декоратора @click.pass_context. Крім того, існує щось, що називається Глобальний контекстний доступ : click.pocoo.org/6/advanced/#global-context-access .
jdno

6
Я склав MWE, використовуючи вказівки @jdno. Ви можете знайти його тут
Дрор

Як я можу згладити всі команди групи? Я маю на увазі всі команди першого рівня.
Mithril

3
@Mithril Використовуйте a CommandCollection. Відповідь Оскара має приклад, і в документації по кліку є дуже приємний: click.palletsprojects.com/en/7.x/commands/… .
jdno

36

Припустимо, ваш проект має таку структуру:

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, яка живе всередині файлу cloudflare.py. Ви 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 та іншими лінтерами, тому що вам не потрібно імпортувати те, що ви б не використовували, і вам не потрібно імпортувати * з будь-якого місця.


Підкажіть, будь ласка, що вставити у файли підкоманди? Мені доводиться імпортувати main cliз init.py, але це призводить до кругового імпорту. Не могли б ви пояснити, як це зробити?
grundic

@grundic Перевірте мою відповідь, якщо ви ще не знайшли рішення. Це може поставити вас на правильний шлях.
jdno

1
@grundic Я сподіваюся, ти вже зрозумів, але у своїх підкомандних файлах ти просто створюєш новий, click.groupякий імпортуєш у CLI верхнього рівня.
Оскар Девід Арбелаес

5

Мені знадобився деякий час, щоб це зрозуміти, але я зрозумів, що поставив це тут, щоб нагадати собі, коли я забуду, як це зробити, і я знову думаю, що частина проблеми полягає в тому, що функція add_command згадується на сторінці github click, але не головна сторінка з прикладами

спочатку дозволяє створити початковий файл python, який називається 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 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 безпосередньо, а також я включив туди основний вислів


0

Я не експерт у галузі кліків, але це має працювати, просто імпортуючи ваші файли в основний. Я б перемістив усі команди в окремі файли, а один основний файл імпортував інші. Так легше контролювати точний порядок, якщо це важливо для вас. Отже, ваш основний файл просто виглядатиме так:

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

(на даний момент у мене є лише одна підкоманда на файл)

У цій статті 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)

Я не знаю, наскільки це надійно, якби я розробляв команду, яка мала кілька рівнів вкладеності та перемикання контексту. Але це, здається, працює добре зараз :)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.