Vergleich weniger populärer und nicht sehr CLI-Bibliotheken: Cliff, Plac, Plumbum und andere (Teil 2)

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


Github
Die Dokumentation
Viele 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.

Teams
Der 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) #     'there is no todos'   #      return ( ("Number", "Title", "Status"), [[task.number, task.title, "" if task.done else "✘"] for task in tasks], ) class Done(Command): """Mark task as done.""" def extend_parser(self, parser): parser.add_argument("number", type=int, help="Task number") def take_action(self, parsed_args): task = self.app.todoapp.task_done(number=parsed_args.number) print(task, "marked as done.") #   Done    class Remove(Done): """Remove task from the list.""" def take_action(self, parsed_args): task = self.app.todoapp.remove_task(number=parsed_args.number) print(task, "removed from the list.") 


Anwendung und Haupt

Die 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): #   add_command, CommandManager  #    setuptools entrypoint manager = CommandManager("todo_cliff") manager.add_command("add", Add) manager.add_command("show", Show) manager.add_command("done", Done) manager.add_command("remove", Remove) super().__init__( description="Todo notes on cliff", version=__version__, command_manager=manager, deferred_help=True, ) self.todoapp = None def initialize_app(self, argv): self.todoapp = TodoApp.fromenv() def clean_up(self, cmd, result, err): self.todoapp.save() def main(args=sys.argv[1:]) -> int: app = App() return app.run(argv=args) 


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 

Testen

Es 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 dagegen

Vorteile:

  • 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


Github
Die Dokumentation

Auf 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 main

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


Testen

Das 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 dagegen

Vorteile:

  • 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


Github
Die Dokumentation
CLI-Dokumentation
Plumbum 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'] #  plumbum     cmd,      >>> from plumbum.cmd import rm, ls, grep, wc >>> rm["-r", "console_examples.egg-info"]() '' >>> chain = ls["-a"] | grep["-v", "\\.py"] | wc["-l"] >>> chain() '11\n' 

Befehle und main

Plumbum 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: # will be ``None`` if no sub-command follows print(colors.red | "No command given.") return 1 class Command(cli.Application): """Command with todoapp object""" def __init__(self, executable): super().__init__(executable) self.todoapp = todolib.TodoApp.fromenv() atexit.register(self.todoapp.save) def log_task(self, task, msg): print("Task", colors.green | task.title, msg, end=".\n") @App.subcommand("add") class Add(Command): """Add new task""" def main(self, task): task = self.todoapp.add_task(title=task) self.log_task(task, "added to the list") @App.subcommand("show") class Show(Command): """Show current tasks""" show_done = cli.Flag("--show-done", help="Include done tasks") def main(self): self.todoapp.print_tasks(self.show_done) @App.subcommand("done") class Done(Command): """Mark task as done""" def main(self, number: int): task = self.todoapp.task_done(number) self.log_task(task, "marked as done") @App.subcommand("remove") class Remove(Command): """Remove task from the list""" def main(self, number: int): task = self.todoapp.remove_task(number) self.log_task(task, "removed from the list.") if __name__ == '__main__': App.run() 


Testen

Das 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 dagegen

Vorteile:

  • 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


Github
Die Dokumentation

Zunä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 main

cmd2 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 Modus
Der 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() 


Testen

Es 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 dagegen

Vorteile:

  • 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


Github
Die Dokumentation
Tutorial
Programmbeispiele

Urwid 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 Teams

Urwid 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", #       ,  #      Button("New task", on_press=add), Button("List tasks", on_press=list_tasks), ) return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def new_menu(self, title, *items): self.new_box(menu(title, *items)) def new_box(self, widget): self.box_level += 1 # overlay      , #     LineBox    self.original_widget = urwid.Overlay( # LineBox  unicode-    self.original_widget, align="center", width=30, valign="middle", height=10, ) def popup(self, text): self.new_menu(text, Button("To menu", on_press=lambda _: self.pop(levels=2))) def keypress(self, size, key): if key != "esc": super().keypress(size, key=key) elif self.box_level > 0: self.pop() def pop(self, levels=1): for _ in range(levels): self.original_widget = self.original_widget[0] self.box_level -= levels if self.box_level == 0: raise urwid.ExitMainLoop() 


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}]" #         button = Button(text, on_press=task_actions, user_data=task.number) buttons.append(button) app.new_menu("Task list", *buttons) def task_actions(button, number): def done(button, number): app.todoapp.task_done(number) app.popup("Task marked as done.") def remove(button, number): app.todoapp.remove_task(number) app.popup("Task removed from the list.") btn_done = Button("Mark as done", on_press=done, user_data=number) btn_remove = Button("Remove from the list", on_press=remove, user_data=number) app.new_menu("Actions", btn_done, btn_remove) 


Haupt
 if __name__ == "__main__": try: with app: urwid.MainLoop(app).run() except KeyboardInterrupt: pass 

Dafür und dagegen

Vorteile:

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

Source: https://habr.com/ru/post/de469093/


All Articles