Im Python-Ökosystem gibt es viele Pakete für CLI-Anwendungen, sowohl beliebte wie Click als auch weniger. Die häufigsten wurden in einem
früheren Artikel berücksichtigt, aber wenig bekannte, aber nicht weniger interessante werden hier gezeigt.

Wie im ersten Teil wird für jede Bibliothek in Python 3.7 ein Konsolenskript für die todolib-Bibliothek geschrieben. Darüber hinaus wird für jede Implementierung ein trivialer Test mit diesen Fixtures geschrieben:
@pytest.fixture(autouse=True) def db(monkeypatch): """ monkeypatch , """ value = {"tasks": []} monkeypatch.setattr(todolib.TodoApp, "save", lambda _: ...) monkeypatch.setattr(todolib.TodoApp, "get_db", lambda _: value) return value @pytest.yield_fixture(autouse=True) def check(db): """ """ yield assert db["tasks"] and db["tasks"][0]["title"] == "test" # , EXPECTED = "Task 'test' created with number 1.\n"
Der gesamte Quellcode ist in
diesem Repository verfügbar.
Klippe
GithubDie DokumentationViele haben von OpenStack gehört, einer Open Source-Plattform für IaaS. Das meiste davon ist in Python geschrieben, einschließlich Konsolendienstprogrammen, die die CLI-Funktionalität seit langer Zeit wiederholen. Dies wurde fortgesetzt, bis Cliff oder das Command Line Interface Formulation Framework als gemeinsames Framework angezeigt wurde. Damit kombinierten Openstack-Entwickler Pakete wie Python-Novaclient, Python-Swiftclient und Python-Keystoneclient in einem
Openstack- Programm.
TeamsDer Ansatz zum Deklarieren von Befehlen ähnelt Cement und Cleo: Argparse als Parameterparser, und die Befehle selbst werden durch die Vererbung der Command-Klasse erstellt. Gleichzeitig gibt es kleine Erweiterungen für die Command-Klasse, z. B. Lister, die die Daten unabhängig formatieren.
Quellcode from cliff import command from cliff.lister import Lister class Command(command.Command): """Command with a parser shortcut.""" def get_parser(self, prog_name): parser = super().get_parser(prog_name) self.extend_parser(parser) return parser def extend_parser(self, parser): ... class Add(Command): """Add new task.""" def extend_parser(self, parser): parser.add_argument("title", help="Task title") def take_action(self, parsed_args): task = self.app.todoapp.add_task(parsed_args.title) print(task, "created with number", task.number, end=".\n") class Show(Lister, Command): """Show current tasks.""" def extend_parser(self, parser): parser.add_argument( "--show-done", action="store_true", help="Include done tasks" ) def take_action(self, parsed_args): tasks = self.app.todoapp.list_tasks(show_done=parsed_args.show_done)
Anwendung und HauptDie Anwendungsklasse verfügt
über die Methoden initialize_app und
clean_up . In unserem Fall initialisieren sie die Anwendung und speichern die Daten.
Quellcode from cliff import app from cliff.commandmanager import CommandManager from todolib import TodoApp, __version__ class App(app.App): def __init__(self):
Arbeitsbeispiele igor$ ./todo_cliff.py add "sell the old laptop" Using database file /home/igor/.local/share/todoapp/db.json Task 'sell the old laptop' created with number 0. Saving database to a file /home/igor/.local/share/todoapp/db.json
Abmelden! Und wenn Sie
unter die Haube schauen, können Sie sehen, dass es auf clevere Weise gemacht wurde: Informationen in stdout und Warnung / Fehler in stderr und, falls erforderlich, durch das Flag
--quiet deaktiviert.
igor$ ./todo_cliff.py -q show +--------+----------------------+--------+ | Number | Title | Status | +--------+----------------------+--------+ | 1 | sell the old laptop | ✘ | +--------+----------------------+--------+
Wie bereits erwähnt, formatiert Lister die Daten, aber die Tabelle ist nicht beschränkt auf:
igor$ ./todo_cliff.py -q show -f json --noindent [{"Number": 0, "Title": "sell old laptop", "Status": "\u2718"}]
Neben json und table stehen yaml und csv zur Verfügung.
Es gibt auch eine versteckte Standardspur:
igor$ ./todo_cliff.py -q remove 3 No such task.
Eine weitere verfügbare REPL- und Fuzzy-Suche, auch bekannt als
Fuzzy-Suche :
igor$ ./todo_cliff.py -q (todo_cliff) help Shell commands (type help %topic%): =================================== alias exit history py quit shell unalias edit help load pyscript set shortcuts Application commands (type help %topic%): ========================================= add complete done help remove show (todo_cliff) whow todo_cliff: 'whow' is not a todo_cliff command. See 'todo_cliff --help'. Did you mean one of these? show
TestenEs ist ganz einfach: Ein App-Objekt wird erstellt und run () wird ebenfalls aufgerufen, wodurch ein Exit-Code zurückgegeben wird.
def test_cliff(capsys): app = todo_cliff.App() code = app.run(["add", "test"]) assert code == 0 out, _ = capsys.readouterr() assert out == EXPECTED
Dafür und dagegenVorteile:
- Verschiedene Annehmlichkeiten sofort einsatzbereit;
- Entwickelt von OpenStack;
- Interaktiver Modus;
- Erweiterbarkeit durch setuptools entrypoint und CommandHook;
- Sphinx-Plugin für die automatische Dokumentation zu CLI;
- Befehlsabschluss (nur Bash);
Nachteile:
- Eine kleine Dokumentation, die im Wesentlichen aus einem detaillierten, aber einzelnen Beispiel besteht;
Ein weiterer Fehler wurde festgestellt: Im Falle eines Fehlers beim Ausblenden des Stack-Trace ist der Exit-Code immer Null.
Plac
GithubDie DokumentationAuf den ersten Blick scheint Plac so etwas wie Feuer zu sein, aber
in Wirklichkeit ist es wie Feuer, das das gleiche Argument und vieles mehr unter der Haube verbirgt.
Plac folgt unter Berufung auf die Dokumentation: "Das alte Prinzip der Computerwelt:
Programme sollten nur gewöhnliche Fälle lösen und das Einfache sollte einfach bleiben und der Komplex gleichzeitig erreichbar sein ." Der Autor des Frameworks verwendet Python seit mehr als neun Jahren und hat es mit der Erwartung geschrieben, „99,9% der Aufgaben“ zu lösen.
Befehle und mainAufmerksamkeit für Anmerkungen in der show- und done-Methode: So analysiert Plac die Parameter- bzw. Argumenthilfe.
Quellcode import plac import todolib class TodoInterface: commands = "add", "show", "done", "remove" def __init__(self): self.app = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.app.save() def add(self, task): """ Add new task. """ task = self.app.add_task(title=task) print(task, "created with number", task.number, end=".\n") def show(self, show_done: plac.Annotation("Include done tasks", kind="flag")): """ Show current tasks. """ self.app.print_tasks(show_done=show_done) def done(self, number: "Task number"): """ Mark task as done. """ task = self.app.task_done(number=int(number)) print(task, "marked as done.") def remove(self, number: "Task number"): """ Remove task from the list. """ task = self.app.remove_task(number=int(number)) print(task, "removed from the list.") if __name__ == "__main__": plac.Interpreter.call(TodoInterface)
TestenDas Testen des Exit-Codes Plac ist leider nicht möglich. Aber das Testen selbst ist real:
def test_plac(capsys): plac.Interpreter.call(todo_plac.TodoInterface, arglist=["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Dafür und dagegenVorteile:
- Einfache Verwendung;
- Interaktiver Modus mit Readline-Unterstützung;
- Stabile API
- Hervorragende Dokumentation;
Das Interessanteste an Plac ist jedoch im
fortgeschrittenen Gebrauch verborgen:
- Ausführung mehrerer Befehle in Threads und Unterprozessen;
- Paralleles Rechnen;
- Telnet-Server;
Nachteile:
- Sie können den Exit-Code nicht testen.
- Schlechtes Projektleben.
Plumbum
GithubDie DokumentationCLI-DokumentationPlumbum ist in der Tat kein so wenig bekanntes Framework - fast 2000 Sterne, und es gibt etwas zu lieben, da es grob gesagt die UNIX-Shell-Syntax implementiert. Nun, mit Zusatzstoffen:
>>> from plumbum import local >>> output = local["ls"]() >>> output.split("\n")[:3] ['console_examples.egg-info', '__pycache__', 'readme.md']
Befehle und mainPlumbum hat auch Tools für die CLI, und um nicht zu sagen, dass sie nur eine Ergänzung sind: Es gibt Nargs, Befehle und Farben:
Quellcode from plumbum import cli, colors class App(cli.Application): """Todo notes on plumbum.""" VERSION = todolib.__version__ verbosity = cli.CountOf("-v", help="Increase verbosity") def main(self, *args): if args: print(colors.red | f"Unknown command: {args[0]!r}.") return 1 if not self.nested_command:
TestenDas Testen von Anwendungen auf Plumbum unterscheidet sich von anderen, außer dass die Notwendigkeit besteht, auch den Namen der Anwendung zu übergeben, d. H. erstes Argument:
def test_plumbum(capsys): _, code = todo_plumbum.App.run(["todo_plumbum", "add", "test"], exit=False) assert code == 0 out, _ = capsys.readouterr() assert out == "Task test created with number 0.\n"
Dafür und dagegenVorteile:
- Hervorragendes Toolkit für die Arbeit mit externen Teams;
- Unterstützung für Stile und Farben;
- Stabile API
- Aktives Leben des Projekts;
Es wurden keine Mängel festgestellt.
cmd2
GithubDie DokumentationZunächst ist cmd2 eine Erweiterung über
cmd aus der Standardbibliothek.
d.h. Es ist für interaktive Anwendungen vorgesehen. Trotzdem ist es in der Überprüfung enthalten, da es auch für den normalen CLI-Modus konfiguriert werden kann.
Befehle und maincmd2 erfordert die Einhaltung einer bestimmten Regel: Befehle sollten mit dem Präfix
do_ beginnen , ansonsten ist alles klar:
interaktiver Modus import cmd2 import todolib class App(cmd2.Cmd): def __init__(self, **kwargs): super().__init__(**kwargs) self.todoapp = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def do_add(self, title): """Add new task.""" task = self.todoapp.add_task(str(title)) self.poutput(f"{task} created with number {task.number}.") def do_show(self, show_done): """Show current tasks.""" self.todoapp.print_tasks(bool(show_done)) def do_done(self, number): """Mark task as done.""" task = self.todoapp.task_done(int(number)) self.poutput(f"{task} marked as done.") def do_remove(self, number): """Remove task from the list.""" task = self.todoapp.remove_task(int(number)) self.poutput(f"{task} removed from the list.") def main(**kwargs): with App(**kwargs) as app: app.cmdloop() if __name__ == '__main__': main()
nicht interaktiver ModusDer normale Modus erfordert etwas mehr Bewegung.
Sie müssen beispielsweise zu argparse zurückkehren und die Logik für den Fall schreiben, dass das Skript ohne Parameter aufgerufen wird. Und jetzt bekommen die Befehle
argparse.Namespace .
Der Parser stammt aus dem Argparse-Beispiel mit kleinen Ergänzungen - jetzt sind Unterparser Attribute des HauptargumentParser.
import cmd2 from todo_argparse import get_parser parser = get_parser(progname="todo_cmd2_cli") class App(cmd2.Cmd): def __init__(self, **kwargs): super().__init__(**kwargs) self.todoapp = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def do_add(self, args): """Add new task.""" task = self.todoapp.add_task(args.title) self.poutput(f"{task} created with number {task.number}.") def do_show(self, args): """Show current tasks.""" self.todoapp.print_tasks(args.show_done) def do_done(self, args): """Mark task as done.""" task = self.todoapp.task_done(args.number) self.poutput(f"{task} marked as done.") def do_remove(self, args): """Remove task from the list.""" task = self.todoapp.remove_task(args.number) self.poutput(f"{task} removed from the list.") parser.add.set_defaults(func=do_add) parser.show.set_defaults(func=do_show) parser.done.set_defaults(func=do_done) parser.remove.set_defaults(func=do_remove) @cmd2.with_argparser(parser) def do_base(self, args): func = getattr(args, "func", None) if func: func(self, args) else: print("No command provided.") print("Call with --help to get available commands.") def main(argv=None): with App() as app: app.do_base(argv or sys.argv[1:]) if __name__ == '__main__': main()
TestenEs wird nur ein interaktives Skript getestet.Da das Testen interaktiver Anwendungen eine große Anzahl von Personal- und Zeitressourcen erfordert, haben cmd2-Entwickler versucht, dieses Problem mithilfe von
Transkriptionen zu lösen - Textdateien mit Beispielen für Eingabe und erwartete Ausgabe. Zum Beispiel:
(Cmd) add test Task 'test' created with number 0.
Daher muss lediglich die Liste der Dateien mit Transkriptionen an die
App übertragen werden :
def test_cmd2(): todo_cmd2.main(transcript_files=["tests/transcript.txt"])
Dafür und dagegenVorteile:
- Gute API für interaktive Anwendungen;
- In den letzten Jahren aktiv weiterentwickelt;
- Ein origineller Testansatz;
- Gute Dokumentation.
Nachteile:
- Erfordert Argparse und zusätzlichen Code beim Schreiben nicht interaktiver Anwendungen.
- Instabile API;
- An einigen Stellen ist die Dokumentation leer.
Bonus: Urwid
GithubDie DokumentationTutorialProgrammbeispieleUrwid ist ein Framework aus einer etwas anderen Welt - von Flüchen und npyscreen, d. H. Von der Konsolen- / Terminal-Benutzeroberfläche. Trotzdem wird es als Bonus in die Bewertung aufgenommen, da es meiner Meinung nach Beachtung verdient.
App und TeamsUrwid verfügt über eine große Anzahl von Widgets, jedoch nicht über Konzepte wie ein Fenster oder einfache Tools für den Zugriff auf benachbarte Widgets. Wenn Sie also ein schönes Ergebnis erzielen möchten, müssen Sie sorgfältig überlegen und / oder andere Pakete verwenden. Andernfalls müssen Sie Daten in den Attributen der Schaltflächen wie hier übertragen:
Quellcode import urwid from urwid import Button import todolib class App(urwid.WidgetPlaceholder): max_box_levels = 4 def __init__(self): super().__init__(urwid.SolidFill()) self.todoapp = None self.box_level = 0 def __enter__(self): self.todoapp = todolib.TodoApp.fromenv() self.new_menu( "Todo notes on urwid",
Teams app = App() def menu(title, *items) -> urwid.ListBox: body = [urwid.Text(title), urwid.Divider()] body.extend(items) return urwid.ListBox(urwid.SimpleFocusListWalker(body)) def add(button): edit = urwid.Edit("Title: ") def handle(button): text = edit.edit_text app.todoapp.add_task(text) app.popup("Task added") app.new_menu("New task", edit, Button("Add", on_press=handle)) def list_tasks(button): tasks = app.todoapp.list_tasks(show_done=True) buttons = [] for task in tasks: status = "done" if task.done else "not done" text = f"{task.title} [{status}]"
Haupt if __name__ == "__main__": try: with app: urwid.MainLoop(app).run() except KeyboardInterrupt: pass
Dafür und dagegenVorteile:
- Hervorragende API zum Schreiben verschiedener TUI-Anwendungen;
- Lange Entwicklungsgeschichte (seit 2010) und stabile API;
- Kompetente Architektur;
- Gute Dokumentation, es gibt Beispiele.
Nachteile:
- Wie zu testen ist unklar. Es fallen mir nur die tmux-Sendeschlüssel ein .
- Nicht informative Fehler, wenn Widgets nicht richtig angeordnet sind.
* * *
Cliff ist Cleo und Cement sehr ähnlich und eignet sich im Allgemeinen für große Projekte.
Ich persönlich würde es nicht wagen, Plac zu verwenden, aber ich empfehle, den Quellcode zu lesen.
Plumbum verfügt über ein praktisches CLI-Toolkit und eine brillante API zum Ausführen anderer Befehle. Wenn Sie also Shell-Skripte in Python neu schreiben, benötigen Sie diese.
cmd2 eignet sich gut als Basis für interaktive Anwendungen und für diejenigen, die vom Standard-cmd migrieren möchten.
Und Urwid bietet schöne und benutzerfreundliche Konsolenanwendungen.
Die folgenden Pakete wurden nicht in die Überprüfung einbezogen:
- Aioconsole - kein nicht interaktiver Modus;
- pyCLI - keine Unterstützung für Unterbefehle;
- Clint - keine Unterstützung für Unterbefehle, Repository im Archiv;
- Kommandozeile - es ist zu alt (letzte Veröffentlichung im Jahr 2009) und uninteressant;
- CLIArgs - alt (letzte Veröffentlichung im Jahr 2010)
- opterator - relativ alt (letzte Veröffentlichung im Jahr 2015)