Как я могу разделить мои команды 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для этого является то, что он объединяет ваши команды и работает только с группами команд. Лучшая альтернатива 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и использовать их здесь, тем самым предоставив вашему интерфейсу командной строки всю мощь классов и модулей Python.

My cli.py- это точка входа для всего интерфейса командной строки:

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

jdno
источник
как передать контекст подкоманде, если они находятся в отдельных модулях?
Vishal
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 Используйте файл 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откуда-то еще?
Erdin Эрай
Это отличная информация, которую я искал. Еще один хороший пример того, как различать группы команд, можно найти здесь: 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, но не в основном страница примеров

сначала давайте создадим начальный файл 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)

Я не знаю, насколько это было бы надежно, если бы я проектировал команду с несколькими уровнями вложенности и переключения контекста. Но пока вроде работает нормально :)

Данькоу
источник