
Python ist eine großartige Sprache für Konsolenanwendungen und hebt eine große Anzahl von Bibliotheken für diese Aufgaben hervor.
Aber welche Bibliotheken gibt es? Und was ist besser zu nehmen? Dieses Material vergleicht beliebte und nicht sehr Tools für die Konsolenwelt und gibt einen Versuch, die zweite Frage zu beantworten.
Zur Vereinfachung des Lesens ist die Rezension in zwei Beiträge unterteilt: Der erste vergleicht die sechs beliebtesten Bibliotheken, der zweite - weniger beliebt und spezifischer, aber dennoch bemerkenswert.
In jedem der Beispiele wird in Python 3.7 ein Konsolendienstprogramm für die
todolib- Bibliothek geschrieben, mit dem Sie Aufgaben erstellen, anzeigen, markieren und löschen können. Der Rest wird hinzugefügt, sofern die Implementierung in einem bestimmten Rahmen einfach ist. Die Aufgaben selbst werden in einer JSON-Datei gespeichert, die in einem separaten Aufruf gespeichert wird - eine zusätzliche Bedingung für die Beispiele.
Zusätzlich wird für jede Implementierung ein trivialer Test geschrieben. Pytest mit den folgenden Vorrichtungen wurde als Testrahmen genommen:
@pytest.fixture(autouse=True) def db(monkeypatch): """ monkeypatch , """ value = {"tasks": []} 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"
Im Prinzip reichen alle oben genannten Punkte aus, um die Bibliotheken zu demonstrieren. Der vollständige Quellcode ist in
diesem Repository verfügbar.
Argparse
Argparse hat einen unbestreitbaren Vorteil - es befindet sich in der Standardbibliothek und seine API ist nicht schwer zu erlernen: Es gibt einen Parser, es gibt Argumente, die Argumente haben
Typ ,
Aktion , Ziel,
Standard und
Hilfe . Und es gibt einen
Subparser - die Fähigkeit, einen Teil der Argumente und der Logik in separate Befehle zu trennen.
Parser
Auf den ersten Blick - nichts Ungewöhnliches - der Parser ist wie ein Parser. Aber meiner Meinung nach ist die Lesbarkeit im Vergleich zu anderen Bibliotheken nicht die beste, weil Argumente für verschiedene Befehle werden an einer Stelle beschrieben.
Quellcode def get_parser(): parser = argparse.ArgumentParser("Todo notes - argparse version") parser.add_argument( "--verbose", "-v", action="store_true", help="Enable verbose mode" ) parser.add_argument("--version", "-V", action="store_true", help="Show version") subparsers = parser.add_subparsers(title="Commands", dest="cmd") add = subparsers.add_parser("add", help="Add new task") add.add_argument("title", help="Todo title") show = subparsers.add_parser("show", help="Show tasks") show.add_argument( "--show-done", action="store_true", help="Include done tasks in the output" ) done = subparsers.add_parser("done", help="Mark task as done") done.add_argument("number", type=int, help="Task number") remove = subparsers.add_parser("remove", help="Remove task") remove.add_argument("number", type=int, help="Task number") return parser
Haupt
Und hier das Gleiche - der Parser kann außer dem Parsen von Argumenten nichts mehr tun, daher muss die Logik unabhängig und an einer Stelle geschrieben werden. Einerseits - es ist möglich zu leben, andererseits - ist es besser möglich, aber es ist noch nicht klar, wie.
UPD: Wie Foldr bemerkt hat, können Subparser Funktionen über set_defaults (func = foo) festlegen, dh mit argparse können Sie main auf kleine Größen kürzen. Lebe und lerne.Quellcode def main(raw_args=None): """ Argparse example entrypoint """ parser = get_parser() args = parser.parse_args(raw_args) logging.basicConfig() if args.verbose: logging.getLogger("todolib").setLevel(logging.INFO) if args.version: print(lib_version) exit(0) cmd = args.cmd if not cmd: parser.print_help() exit(1) with TodoApp.fromenv() as app: if cmd == "add": task = app.add_task(args.title) print(task, "created with number", task.number, end=".\n") elif cmd == "show": app.print_tasks(args.show_done) elif cmd == "done": task = app.task_done(args.number) print(task, "marked as done.") elif cmd == "remove": task = app.remove_task(args.number) print(task, "removed from list.")
Testen
Um die Ausgabe des Dienstprogramms zu überprüfen, wird das
Capsys- Fixture
verwendet , das den Zugriff auf Text von stdout und stderr ermöglicht.
def test_argparse(capsys): todo_argparse.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Zusammenfassung
Von den Vorteilen - eine gute Reihe von Funktionen zum Parsen, das Vorhandensein eines Moduls in der Standardbibliothek.
Cons - argparse ist nur an der Analyse von Argumenten beteiligt, der größte Teil der Logik musste von mir selbst geschrieben werden. Und es ist unklar, wie der Exit-Code in Tests getestet werden soll.
docopt
docopt ist ein kleiner Parser (<600 Zeilen im Vergleich zu 2500 mit Argparse), der Sie zum Lächeln bringt und eine Beschreibung auf GitHub zitiert. Die Hauptidee von docopt besteht darin, die Schnittstelle wörtlich mit Text zu beschreiben, beispielsweise in docstring.
Auf demselben Github, docopt> 6700 Sterne, wird es in mindestens 22.000 anderen Projekten verwendet. Und das nur mit der Python-Implementierung! Die docopt-Projektseite bietet viele Optionen für verschiedene Sprachen, von C und PHP über CoffeeScript bis hin zu R. Eine solche plattformübergreifende Lösung kann nur durch die Kompaktheit und Einfachheit des Codes erklärt werden.
Parser
Im Vergleich zu Argparse ist dieser Parser ein großer Schritt nach vorne.
"""Todo notes on docopt. Usage: todo_docopt [-v | -vv ] add <task> todo_docopt [-v | -vv ] show --show-done todo_docopt [-v | -vv ] done <number> todo_docopt [-v | -vv ] remove <number> todo_docopt -h | --help todo_docopt --version Options: -h --help Show help. -v --verbose Enable verbose mode. """
Haupt
Im Allgemeinen ist alles das gleiche wie bei argparse, aber jetzt kann
verbose mehrere Werte (0-2) haben, und der Zugriff auf die Argumente ist unterschiedlich: docopt gibt keinen Namespace mit Attributen zurück, sondern nur ein Wörterbuch, in dem die Auswahl eines Befehls angegeben ist durch ihren Booleschen Wert, wie in:
Quellcode def main(argv=None): args = docopt(__doc__, argv=argv, version=lib_version) log.setLevel(levels[args["--verbose"]]) logging.basicConfig() log.debug("Arguments: %s", args) with TodoApp.fromenv() as app: if args["add"]: task = app.add_task(args["<task>"]) print(task, "created with number", task.number, end=".\n") elif args["show"]: app.print_tasks(args["--show-done"]) elif args["done"]: task = app.task_done(args["<number>"]) print(task, "marked as done.") elif args["remove"]: task = app.remove_task(args["<number>"]) print(task, "removed from list.")
Testen
Ähnlich wie beim Argparse-Test:
def test_docopt(capsys): todo_docopt.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Zusammenfassung
Von den Vorteilen - viel weniger Code für den Parser, einfache Beschreibung und Lesen von Befehlen und Argumenten, integrierte Version.
Nachteile, erstens das gleiche wie argparse - viel Logik in
main , können Sie Exit-Code nicht testen. Darüber hinaus ist die aktuelle Version (0.6.2) von docopt noch nicht stabil und wird es wahrscheinlich nie sein - das Projekt wurde von 2012 bis Ende 2013 aktiv weiterentwickelt, das letzte Commit war am 17. Dezember. Und das Unangenehmste im Moment ist, dass einige Docopt-Stammgäste bei der Durchführung von Tests DeprecationWarning provozieren.
Klicken Sie auf
Click unterscheidet sich grundlegend von argparse und docopt in der Menge an Funktionen und dem Ansatz zur Beschreibung von Befehlen und Parametern durch Dekoratoren, und es wird vorgeschlagen, die Logik selbst in separate Funktionen anstelle einer großen
Hauptfunktion zu unterteilen . Die Autoren behaupten, dass Click viele Einstellungen hat, aber die Standardparameter sollten ausreichen. Unter den Funktionen werden die verschachtelten Befehle und ihr verzögertes Laden hervorgehoben.
Das Projekt ist äußerst beliebt: Zusätzlich zu> 8100 Sternen und der Verwendung in mindestens 174.000 (!) Projekten befindet es sich noch in der Entwicklung: Version 7.0 wurde im Herbst 2018 veröffentlicht, und bis heute werden neue Commits und Zusammenführungsanforderungen angezeigt Tag.
Parser
Auf der Dokumentationsseite habe ich den Decorator "configuration_option" gefunden, der den Benutzer vor der Ausführung des Befehls um Bestätigung bittet. Um dies zu demonstrieren, wurde der Befehl zum Löschen hinzugefügt, mit dem die gesamte Aufgabenliste gelöscht wird.
Quellcode levels = [logging.WARN, logging.INFO, logging.DEBUG] pass_app = click.make_pass_decorator(TodoApp) @click.group() @click.version_option(lib_version, prog_name="todo_click") @click.option("-v", "--verbose", count=True)
Haupt
Und hier treffen wir auf den Hauptvorteil von Click - aufgrund der Tatsache, dass die Logik der Befehle entsprechend ihrer Funktionen verteilt ist, bleibt fast nichts in main. Hier wird auch die Fähigkeit der Bibliothek demonstriert, Argumente und Parameter von Umgebungsvariablen zu empfangen.
if __name__ == "__main__": cli(auto_envvar_prefix="TODO")
Testen
Im Fall von Click muss sys.stdout nicht abgefangen werden, da für solche Dinge ein
click.testing- Modul mit einem Runner vorhanden ist. Und
CliRunner selbst fängt nicht nur die Ausgabe ab, sondern ermöglicht es Ihnen auch, den Exit-Code zu überprüfen, was auch cool ist. All dies ermöglicht das Testen von Click-Utilities ohne Verwendung von Pytest und Umgehen des Standard-
Unittest- Moduls.
import click.testing def test_click(): runner = click.testing.CliRunner() result = runner.invoke(todo_click.cli, ["add", "test"]) assert result.exit_code == 0 assert result.output == EXPECTED
Zusammenfassung
Dies ist nur ein kleiner Teil dessen, was Click leisten kann. Aus dem Rest der API - Validierung von Werten, Integration in das Terminal (Farben, Pager a la Less, Fortschrittsbalken usw.), Ergebnisrückruf, automatische Vervollständigung und vieles mehr. Sie können ihre Beispiele
hier sehen .
Vorteile: Viele Tools für jeden Anlass, ein origineller, aber gleichzeitig praktischer Ansatz zur Beschreibung von Teams, einfache Tests und ein aktives Projektleben.
Nachteile: Was sind die Nachteile eines „Klicks“ - dies ist eine schwierige Frage. Vielleicht weiß er nicht, wozu die folgenden Bibliotheken in der Lage sind?
Feuer
Fire ist nicht nur eine junge (2017 erschienene) Bibliothek für Konsolenschnittstellen von Google, sondern eine Bibliothek zum Generieren von Konsolenschnittstellen aus
absolut jedem Python-
Objekt .
Unter anderem wird angegeben, dass Fire bei der Entwicklung und dem Debuggen von Code hilft, zur Anpassung des vorhandenen Codes in der CLI beiträgt, den Übergang von Bash zu Python erleichtert und über eine eigene REPL für interaktive Arbeit verfügt. Sollen wir sehen?
Parser und Haupt
fire.Fire kann wirklich jedes Objekt akzeptieren: ein Modul, eine Klasseninstanz, ein Wörterbuch mit Befehlsnamen und entsprechenden Funktionen usw.
Für uns ist wichtig, dass Fire die Übertragung eines Klassenobjekts ermöglicht. Daher akzeptiert der Klassenkonstruktor Argumente, die allen Befehlen gemeinsam sind, und seine Methoden und Attribute sind separate Befehle. Wir werden dies verwenden:
Quellcode class Commands: def __init__(self, db=None, verbose=False): level = logging.INFO if verbose else logging.WARNING logging.basicConfig(level=level) logging.getLogger("todolib").setLevel(level) self._app = todolib.TodoApp.fromenv(db) atexit.register(self._app.save) def version(self): return todolib.__version__ def add(self, task): """Add new task.""" task = self._app.add_task(task) print(task, "created with number", task.number, end=".\n") def show(self, show_done=False): """ Show current tasks. """ self._app.print_tasks(show_done) def done(self, number): """ Mark task as done. """ task = self._app.task_done(number) print(task, "marked as done.") def remove(self, number): """ Removes task from the list. """ task = self._app.remove_task(number) print(task, "removed from the list.") def main(args=None): fire.Fire(Commands, command=args)
Inline-Flags
Fire hat seine eigenen Flags mit einer speziellen Syntax (sie müssen nach dem "-" übergeben werden), mit denen Sie unter die Haube des Parsers und der gesamten Anwendung schauen können:
Beispiele nennen $ ./todo_fire.py show -- --trace Fire trace: 1. Initial component 2. Instantiated class "Commands" (todo_fire.py:9) 3. Accessed property "show" (todo_fire.py:25) $ ./todo_fire.py -- --verbose | head -n 12
Testen
Das Testen der Hauptfunktion ähnelt dem Testen von Argparse und Docopt, daher verstehe ich den Punkt hier nicht.
Gleichzeitig ist es erwähnenswert, dass Sie aufgrund der introspektiven Natur des Feuers auch die Befehlsklasse sofort testen können.
Zusammenfassung
Feuer ist ein Werkzeug, das nicht weniger interessant ist als Klicken. Es ist nicht erforderlich, viele Optionen im Parser aufzulisten, die Konfiguration ist minimal, es gibt Optionen zum Debuggen, und die Bibliothek selbst
lebt und entwickelt sich noch aktiver als durch Klicken (60 Commits in diesem Sommer).
Nachteile: Kann deutlich weniger als Klick und andere Parser; instabile API (aktuelle Version ist 0.2.1).
Zement
Tatsächlich ist
Cement nicht gerade eine CLI-Bibliothek, sondern ein Framework für Konsolenanwendungen. Es wird jedoch behauptet, dass es für Skripte und komplexe Anwendungen mit verschiedenen Integrationen geeignet ist.
Parser
Der Parser in Cement sieht ungewöhnlich aus, aber wenn Sie sich die Parameter genau ansehen, ist es leicht zu erraten, dass sich der bekannte Argparse unter der Haube befindet. Aber vielleicht ist dies das Beste - Sie müssen keine neuen Parameter lernen.
Quellcode from cement import Controller, ex class Base(Controller): class Meta: label = "base" arguments = [ ( ["-v", "--version"], {"action": "version", "version": f"todo_cement v{todolib.__version__}"}, ) ] def _default(self): """Default action if no sub-command is passed.""" self.app.args.print_help() @ex(help="Add new task", arguments=[(["task"], {"help": "Task title"})]) def add(self): title = self.app.pargs.task self.app.log.debug(f"Task title: {title!r}") task = self.app.todoobj.add_task(title) print(task, "created with number", task.number, end=".\n") @ex( help="Show current tasks", arguments=[ (["--show-done"], dict(action="store_true", help="Include done tasks")) ], ) def show(self): self.app.todoobj.print_tasks(self.app.pargs.show_done) @ex(help="Mark task as done", arguments=[(["number"], {"type": int})]) def done(self): task = self.app.todoobj.task_done(self.app.pargs.number) print(task, "marked as done.") @ex(help="Remove task from the list", arguments=[(["number"], {"type": int})]) def remove(self): task = self.app.todoobj.remove_task(self.app.pargs.number) print(task, "removed from the list.")
App und Haupt
Unter anderem umhüllt Zement Signale immer noch mit Ausnahmen. Dies wird hier am Nullcode-Ausgang mit SIGINT / SIGTERM demonstriert.
Quellcode class TodoApp(App): def __init__(self, argv=None): super().__init__(argv=argv) self.todoobj = None def load_db(self): self.todoobj = todolib.TodoApp.fromenv() def save(self): self.todoobj.save() class Meta:
Wenn Sie sich mit main vertraut machen, können Sie sehen, dass todolib.TodoApp auch im überschriebenen __enter __ / __ exit__ geladen und gespeichert werden kann. Diese Phasen wurden jedoch schließlich in separate Methoden unterteilt, um Zementhaken zu demonstrieren.
Testen
Zum Testen können Sie dieselbe Anwendungsklasse verwenden:
def test_cement(capsys): with todo_cement.TodoApp(argv=["add", "test"]) as app: app.run() out, _ = capsys.readouterr() assert out == EXPECTED
Zusammenfassung
Vorteile: Der Satz von APIs ist wie ein Satz von Schweizer Messern, Erweiterbarkeit durch Hooks und Plugins, eine stabile Schnittstelle und aktive Entwicklung.
Nachteile: Stellenweise leere Dokumentation; kleine zementbasierte Skripte scheinen etwas kompliziert zu sein.
Cleo
Cleo ist weit davon entfernt, ein so beliebtes Framework zu sein wie die anderen hier aufgeführten (insgesamt etwa 400 Sterne auf GitHub), und dennoch habe ich es kennengelernt, als ich studierte, wie Poesie die Ausgabe formatierte.
Cleo ist also eines der Projekte des Autors der bereits erwähnten Poesie, einem Tool zum Verwalten von Abhängigkeiten, virtuellen Umgebungen und Anwendungserstellungen. Über Poesie auf einem Habr, die schon mehr als einmal geschrieben wurde, und über seinen Konsolenteil - nein.
Parser
Cleo basiert wie Cement auf Objektprinzipien, d.h. Befehle werden über die Command-Klasse und ihre Dokumentzeichenfolge definiert, auf Parameter wird über die option () -Methode zugegriffen und so weiter. Darüber hinaus unterstützt die line () -Methode, mit der Text ausgegeben wird, Stile (d. H. Farben) und Ausgabefilterung basierend auf der Anzahl der ausführlichen Flags. Cleo hat auch Tabellenausgabe. Und auch Fortschrittsbalken. Und doch ... Im Allgemeinen siehe:
Quellcode from cleo import Command as BaseCommand
Haupt
Sie müssen
lediglich ein
cleo.Application- Objekt erstellen und dann Befehle an add_commands übergeben. Um dies während des Tests nicht zu wiederholen, wurde dies alles von main auf den Konstruktor übertragen:
from cleo import Application as BaseApplication class TodoApp(BaseApplication): def __init__(self): super().__init__(name="ToDo app - cleo version", version=todolib.__version__) self.add_commands(AddCommand(), ShowCommand(), DoneCommand(), RemoveCommand()) def main(args=None): TodoApp().run(args=args)
Testen
Um Befehle in Cleo zu testen, gibt es
CommandTester , der wie alle erwachsenen
Onkel des Frameworks E / A abfängt und Code beendet:
def test_cleo(): app = todo_cleo.TodoApp() command = app.find("add") tester = cleo.CommandTester(command) tester.execute("test") assert tester.status_code == 0 assert tester.io.fetch_output() == "Task test created with number 0.\n"
Zusammenfassung
Vorteile: Objektstruktur mit Typhinweisen, die die Entwicklung vereinfacht (da viele IDEs und Editoren OOP-Code und Typisierungsmodul gut unterstützen); Eine gute Menge an Funktionen, um nicht nur mit Argumenten, sondern auch mit E / A zu arbeiten.
Plus oder Minus: Der Ausführlichkeitsparameter, der nur mit I / O Cleo / CliKit kompatibel ist. Obwohl Sie einen benutzerdefinierten Handler für das Protokollierungsmodul schreiben können, kann es schwierig sein, ihn zusammen mit der Entwicklung von cleo zu warten.
Nachteile: Offensichtlich - eine persönliche Meinung - eine junge API: Dem Framework fehlt ein anderer "großer" Benutzer, außer Poetry, und Cleo entwickelt sich parallel zur Entwicklung und für die Bedürfnisse eines; Manchmal ist die Dokumentation veraltet (z. B. liegen die Protokollierungsstufen jetzt nicht mehr im clikit-Modul, sondern in clikit.api.io.flags), und im Allgemeinen ist sie schlecht und spiegelt nicht die gesamte API wider.
Cleo konzentriert sich im Vergleich zu Cement mehr auf die CLI und ist der einzige, der über die Formatierung (Ausblenden der Standard-Stack-Ablaufverfolgung) von Ausnahmen in der Standardausgabe nachgedacht hat. Aber er - wieder eine persönliche Meinung - verliert in seiner Jugend und Stabilität der API gegen Cement.
Abschließend
Zu diesem Zeitpunkt hat jeder bereits seine eigene Meinung, was besser ist, aber die Schlussfolgerung sollte lauten: Ich mochte Click am meisten, weil es viele Dinge enthält und es ziemlich einfach ist, Anwendungen damit zu entwickeln und zu testen. Wenn Sie versuchen, Code auf ein Minimum zu schreiben, beginnen Sie mit Fire. Ihr Skript benötigt Zugriff auf Memcached, Formatierung mit Jinja und Erweiterbarkeit - nehmen Sie Cement und Sie werden es nicht bereuen. Sie haben ein Haustierprojekt oder möchten etwas anderes ausprobieren - schauen Sie sich Cleo an.