Jak mogę podzielić moje polecenia Click, każde z zestawem poleceń podrzędnych, na wiele plików?

87

Mam jedną dużą aplikację do klikania, którą opracowałem, ale nawigacja po różnych poleceniach / podpoleceniach staje się trudna. Jak zorganizować moje polecenia w oddzielne pliki? Czy można organizować polecenia i ich podkomendy w oddzielne klasy?

Oto przykład, jak chciałbym to rozdzielić:

w tym

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
Brad T.
źródło

Odpowiedzi:

99

Wadą używania CommandCollectiondo tego jest to, że scala twoje polecenia i działa tylko z grupami poleceń. Imho lepszą alternatywą jest użycie, add_commandaby osiągnąć ten sam rezultat.

Mam projekt z następującym drzewem:

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

Każda podkomenda ma swój własny moduł, co sprawia, że ​​zarządzanie nawet złożonymi implementacjami jest niezwykle łatwe z wieloma dodatkowymi klasami i plikami. W każdym module commands.pyplik zawiera @clickadnotacje. Przykład group2/commands.py:

import click


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

W razie potrzeby możesz łatwo utworzyć więcej klas w module importi używać ich tutaj, dając w ten sposób swojemu CLI pełną moc klas i modułów Pythona.

My cli.pyjest punktem wejścia dla całego 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)

Dzięki tej konfiguracji bardzo łatwo jest oddzielić polecenia według obaw, a także zbudować wokół nich dodatkowe funkcje, których mogą potrzebować. Jak dotąd bardzo mi to służyło ...

Źródła: http://click.pocoo.org/6/quickstart/#nesting-commands

jdno
źródło
jak przekazać kontekst do komendy, jeśli znajdują się one w oddzielnych modułach?
vishal
2
@vishal, zajrzyj do tej sekcji dokumentacji: click.pocoo.org/6/commands/#nested-handling-and-contexts Możesz przekazać obiekt kontekstu do dowolnego polecenia za pomocą dekoratora @click.pass_context. Alternatywnie istnieje również coś, co nazywa się Globalny dostęp do kontekstu : click.pocoo.org/6/advanced/#global-context-access .
jdno
6
Skompilowałem MWE przy użyciu wytycznych @jdno. Możesz go znaleźć tutaj
Dror
Jak mogę spłaszczyć wszystkie polecenia grupowe? Mam na myśli wszystkie polecenia na pierwszym poziomie.
Mithril
3
@Mithril Użyj pliku CommandCollection. Odpowiedź Oscara ma przykład, a w dokumentacji Click jest naprawdę ładny: click.palletsprojects.com/en/7.x/commands/… .
jdno
36

Załóżmy, że Twój projekt ma następującą strukturę:

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

Grupy to nic innego jak wiele poleceń, a grupy można zagnieżdżać. Możesz podzielić swoje grupy na moduły i zaimportować je do init.pypliku i dodać do cligrupy za pomocą add_command.

Oto init.pyprzykład:

import click
from .commands.cloudflare import cloudflare


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


cli.add_command(cloudflare)

Musisz zaimportować grupę cloudflare, która znajduje się w pliku cloudflare.py. Twój commands/cloudflare.pywyglądałby tak:

import click


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


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

Następnie możesz uruchomić polecenie cloudflare w następujący sposób:

$ python init.py cloudflare zone

Te informacje nie są zbyt wyraźne w dokumentacji, ale jeśli spojrzysz na kod źródłowy, który jest bardzo dobrze skomentowany, możesz zobaczyć, jak można zagnieżdżać grupy.

Diego Castro
źródło
5
Zgodzić się. Tak minimalna, że ​​powinna być częścią dokumentacji. Dokładnie to, czego szukałem do tworzenia złożonych narzędzi! Dzięki 🙏!
Simon Kemper,
Z pewnością jest świetny, ale pojawiło się pytanie: biorąc pod uwagę twój przykład, czy powinienem usunąć @cloudflare.command()z zonefunkcji, jeśli importuję zonez innego miejsca?
Erdin Eray
To doskonała informacja, której szukałem. Kolejny dobry przykład, jak rozróżnić grupy dowodzenia, można znaleźć tutaj: github.com/dagster-io/dagster/tree/master/python_modules/ ...
Thomas Klinger
10

Szukam czegoś takiego w tej chwili, w twoim przypadku jest to proste ponieważ masz grupy w każdym z plików, możesz rozwiązać ten problem jak wyjaśniono w dokumentacji :

W init.pypliku:

import click

from command_cloudflare import cloudflare
from command_uptimerobot import uptimerobot

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

if __name__ == '__main__':
    cli()

Najlepszą częścią tego rozwiązania jest to, że jest całkowicie zgodne z pep8 i innymi lintersami, ponieważ nie musisz importować czegoś, czego nie używałbyś i nie musisz importować * z dowolnego miejsca.

Oscar David Arbeláez
źródło
Czy możesz powiedzieć, co umieścić w plikach poleceń podrzędnych? Muszę zaimportować main cliz init.py, ale prowadzi to do importu cyklicznego. Czy mógłbyś wyjaśnić, jak to zrobić?
grundic
@grundic Sprawdź moją odpowiedź, jeśli nie znalazłeś jeszcze rozwiązania. To może postawić cię na właściwej drodze.
jdno
1
@grundic Mam nadzieję, że już zorientowałeś się, ale w swoich podrzędnych plikach poleceń po prostu tworzysz nowy click.group, który importujesz w CLI najwyższego poziomu.
Oscar David Arbeláez
5

Zajęło mi to trochę czasu, aby to rozgryźć, ale pomyślałem, że umieszczę to tutaj, aby przypomnieć sobie, gdy zapomnę, jak to zrobić, i myślę, że częścią problemu jest to, że funkcja add_command jest wymieniona na stronie github kliknięcia, ale nie na stronie głównej strona z przykładami

najpierw stwórzmy początkowy plik Pythona o nazwie 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()

Następnie umieśćmy polecenia narzędzi w pliku o nazwie 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()

Następnie umieśćmy kilka poleceń kompilujących w pliku o nazwie 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()

uruchomienie root.py powinno nam teraz dać

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

  Demo

Options:
  --help  Show this message and exit.

Commands:
  compile  Commands related to compiling
  tools    Tool related commands

uruchomienie "root.py compile" powinno nam dać

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

Zauważysz również, że możesz uruchomić cli_tools.py lub cli_compile.py bezpośrednio, jak również umieściłem tam główną instrukcję

chleb czosnkowy
źródło
0

Nie jestem ekspertem od kliknięć, ale powinno to działać, po prostu importując pliki do głównego. Przeniósłbym wszystkie polecenia do oddzielnych plików, a jeden główny plik importuje pozostałe. W ten sposób łatwiej jest kontrolować dokładną kolejność, jeśli jest to dla Ciebie ważne. Więc twój główny plik wyglądałby tak:

import commands_main
import commands_cloudflare
import commands_uptimerobot
Achim
źródło
0

edycja: właśnie zdałem sobie sprawę, że moja odpowiedź / komentarz to niewiele więcej niż powtórzenie tego, co oferują oficjalne dokumenty firmy Click w sekcji „Niestandardowe polecenia wielofunkcyjne”: https://click.palletsprojects.com/en/7.x/commands/#custom -multi-polecenia

Aby dodać do doskonałej, zaakceptowanej odpowiedzi @jdno, wymyśliłem funkcję pomocniczą, która automatycznie importuje i automatycznie dodaje moduły podkomendy, co znacznie skraca standard w moim cli.py:

Struktura mojego projektu jest następująca:

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

Każdy plik podpoleceń wygląda mniej więcej tak:

import click

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

(na razie mam tylko jedną komendę na plik)

W programie cli.pynapisałem add_subcommand()funkcję, która zapętla każdą ścieżkę pliku globowaną przez "podpolecenia / *. Py", a następnie wykonuje polecenie importu i dodawania.

Oto, do czego uproszczono treść skryptu 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()

I to jest właśnie to add_subcommands() wygląda funkcja:


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)

Nie wiem, jak solidne jest to, gdybym zaprojektował polecenie, które ma kilka poziomów zagnieżdżania i przełączania kontekstów. Ale wydaje się, że na razie działa dobrze :)

dancow
źródło