Wie kann ich meine Click-Befehle mit jeweils mehreren Unterbefehlen in mehrere Dateien aufteilen?

82

Ich habe eine große Klickanwendung, die ich entwickelt habe, aber das Navigieren durch die verschiedenen Befehle / Unterbefehle wird schwierig. Wie organisiere ich meine Befehle in separaten Dateien? Ist es möglich, Befehle und ihre Unterbefehle in separate Klassen zu organisieren?

Hier ist ein Beispiel, wie ich es trennen möchte:

drin

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.
quelle

Antworten:

91

Der Nachteil bei der Verwendung CommandCollectionist, dass Ihre Befehle zusammengeführt werden und nur mit Befehlsgruppen funktionieren. Die imho bessere Alternative ist es add_command, das gleiche Ergebnis zu erzielen.

Ich habe ein Projekt mit folgendem Baum:

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

Jeder Unterbefehl verfügt über ein eigenes Modul, wodurch es unglaublich einfach ist, auch komplexe Implementierungen mit vielen weiteren Hilfsklassen und Dateien zu verwalten. In jedem Modul commands.pyenthält die Datei die @clickAnmerkungen. Beispiel group2/commands.py:

import click


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

Bei Bedarf können Sie problemlos weitere Klassen im Modul erstellen importund diese hier verwenden, sodass Ihre CLI die volle Leistung der Python-Klassen und -Module erhält.

Mein cli.pyist der Einstiegspunkt für die gesamte 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)

Mit diesem Setup ist es sehr einfach, Ihre Befehle nach Bedenken zu trennen und zusätzliche Funktionen zu erstellen, die sie möglicherweise benötigen. Es hat mir bisher sehr gut gedient ...

Referenz: http://click.pocoo.org/6/quickstart/#nesting-commands

jdno
quelle
Wie übergebe ich den Kontext an einen Unterbefehl, wenn diese sich in separaten Modulen befinden?
Vishal
2
@vishal, sehen Sie sich diesen Abschnitt der Dokumentation an: click.pocoo.org/6/commands/#nested-handling-and-contexts Sie können das Kontextobjekt mit dem Dekorator an einen beliebigen Befehl übergeben @click.pass_context. Alternativ gibt es auch einen so genannten globalen Kontextzugriff : click.pocoo.org/6/advanced/#global-context-access .
11.
6
Ich habe eine MWE nach @ jdno-Richtlinien kompiliert. Sie finden es hier
Dror
Wie kann ich alle Gruppenbefehle abflachen? Ich meine, alle Befehle in der ersten Ebene.
Mithril
3
@Mithril Verwenden Sie a CommandCollection. Oskars Antwort hat ein Beispiel, und es gibt ein wirklich schönes in der Dokumentation von click: click.palletsprojects.com/en/7.x/commands/… .
jdno
34

Angenommen, Ihr Projekt hat die folgende Struktur:

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

Gruppen sind nichts anderes als mehrere Befehle und Gruppen können verschachtelt werden. Sie können Ihre Gruppen in Module aufteilen und in Ihre init.pyDatei importieren und climit dem Befehl add_command zur Gruppe hinzufügen.

Hier ist ein init.pyBeispiel:

import click
from .commands.cloudflare import cloudflare


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


cli.add_command(cloudflare)

Sie müssen die Cloudflare-Gruppe importieren, die sich in der Datei cloudflare.py befindet. Ihr commands/cloudflare.pywürde so aussehen:

import click


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


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

Anschließend können Sie den Befehl cloudflare folgendermaßen ausführen:

$ python init.py cloudflare zone

Diese Informationen sind in der Dokumentation nicht sehr explizit. Wenn Sie sich jedoch den Quellcode ansehen, der sehr gut kommentiert ist, können Sie sehen, wie Gruppen verschachtelt werden können.

Diego Castro
quelle
5
Zustimmen. So minimal, dass es Teil der Dokumentation sein sollte. Genau das, wonach ich gesucht habe, um komplexe Werkzeuge zu bauen! Danke 🙏!
Simon Kemper
Es ist sicher toll , aber habe eine Frage: Wenn man bedenkt Ihr Beispiel sollte ich entfernen @cloudflare.command()von zoneFunktion , wenn ich importieren zonevon woanders?
Erdin Eray
Dies ist eine ausgezeichnete Information, nach der ich gesucht habe. Ein weiteres gutes Beispiel für die Unterscheidung zwischen Befehlsgruppen finden Sie hier: github.com/dagster-io/dagster/tree/master/python_modules/…
Thomas Klinger
10

Ich suche im Moment nach so etwas. In Ihrem Fall ist es einfach, weil Sie Gruppen in jeder der Dateien haben. Sie können dieses Problem lösen, wie in der Dokumentation erläutert :

In der init.pyDatei:

import click

from command_cloudflare import cloudflare
from command_uptimerobot import uptimerobot

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

if __name__ == '__main__':
    cli()

Das Beste an dieser Lösung ist, dass sie vollständig mit pep8 und anderen Lintern kompatibel ist, da Sie nichts importieren müssen, was Sie nicht verwenden würden, und nicht * von irgendwoher importieren müssen.

Oscar David Arbeláez
quelle
Können Sie bitte sagen, was in die Unterbefehlsdateien eingefügt werden soll? Ich muss main cliaus init.py importieren, aber dies führt zu zirkulären Importen. Könnten Sie bitte erklären, wie es geht?
Grundic
@grundic Schauen Sie sich meine Antwort an, wenn Sie noch keine Lösung gefunden haben. Es könnte Sie auf den richtigen Weg bringen.
jdno
1
@grundic Ich hoffe, Sie haben es bereits herausgefunden, aber in Ihren Unterbefehlsdateien erstellen Sie einfach eine neue click.group, die Sie in die CLI der obersten Ebene importieren.
Oscar David Arbeláez
5

Ich habe eine Weile gebraucht, um das herauszufinden, aber ich dachte, ich würde das hier einfügen, um mich daran zu erinnern, wenn ich vergesse, wie ich es wieder mache. Ich denke, ein Teil des Problems ist, dass die Funktion add_command auf der Github-Seite von click erwähnt wird, aber nicht auf der Hauptseite Beispielseite

Zuerst erstellen wir eine erste Python-Datei namens 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()

Als nächstes fügen wir einige Tools-Befehle in eine Datei mit dem Namen cli_tools.py ein

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

Als nächstes fügen wir einige Kompilierungsbefehle in eine Datei mit dem Namen cli_compile.py ein

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

Das Ausführen von root.py sollte uns jetzt geben

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

  Demo

Options:
  --help  Show this message and exit.

Commands:
  compile  Commands related to compiling
  tools    Tool related commands

Das Ausführen von "root.py compile" sollte uns geben

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

Sie werden auch feststellen, dass Sie cli_tools.py oder cli_compile.py direkt ausführen können, und ich habe dort eine Hauptanweisung eingefügt

Knoblauchbrot
quelle
0

Ich bin kein Klick-Experte, aber es sollte funktionieren, indem Sie einfach Ihre Dateien in die Hauptdatei importieren. Ich würde alle Befehle in separate Dateien verschieben und eine Hauptdatei die anderen importieren lassen. Auf diese Weise ist es einfacher, die genaue Reihenfolge zu kontrollieren, falls dies für Sie wichtig ist. Ihre Hauptdatei würde also einfach so aussehen:

import commands_main
import commands_cloudflare
import commands_uptimerobot
Achim
quelle
0

Bearbeiten: Ich habe gerade festgestellt, dass meine Antwort / mein Kommentar kaum mehr als eine Wiederholung dessen ist, was die offiziellen Dokumente von Click im Abschnitt "Benutzerdefinierte Multi-Befehle" bieten: https://click.palletsprojects.com/en/7.x/commands/#custom -multi-Befehle

Um die ausgezeichnete, akzeptierte Antwort von @jdno zu ergänzen, habe ich eine Hilfsfunktion entwickelt, die Unterbefehlsmodule automatisch importiert und automatisch hinzufügt, wodurch die Boilerplate in meinem cli.py:

Meine Projektstruktur ist folgende:

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

Jede Unterbefehlsdatei sieht ungefähr so ​​aus:

import click

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

(Im Moment habe ich nur einen Unterbefehl pro Datei)

In cli.pyhabe ich eine add_subcommand()Funktion geschrieben, die jeden Dateipfad durchläuft, der durch "Unterbefehle / *. Py" gekennzeichnet ist, und dann den Befehl import und add ausführt.

Der Text des cli.py-Skripts wird folgendermaßen vereinfacht:

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

Und so add_subcommands()sieht die Funktion aus:


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)

Ich weiß nicht, wie robust dies ist, wenn ich einen Befehl mit mehreren Ebenen der Verschachtelung und Kontextumschaltung entwerfen würde. Aber es scheint jetzt in Ordnung zu funktionieren :)

Dan Nguyen
quelle