Vergleich gängiger CLI-Bibliotheken für Python: Klicken, Zementieren, Feuern und andere



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) # click   ,     @click.option("--db", help="Path to the database file") @click.pass_context def cli(ctx, verbose, db): """Todo notes - click version.""" level = levels[min(verbose, 2)] logging.basicConfig(level=level) logging.getLogger("todolib").setLevel(level) ctx.obj = TodoApp.fromenv(db) atexit.register(ctx.obj.save) @cli.command() @click.argument("task") @pass_app def add(app, task): """ Add new task. """ task = app.add_task(task) click.echo(f"{task} created with number {task.number}.") @cli.command() @click.option("--show-done", is_flag=True, help="Include done tasks") @pass_app def show(app, show_done): """ Show current tasks. """ app.print_tasks(show_done) @cli.command() @click.argument("number", type=int) @pass_app def done(app, number): """ Mark task as done. """ task = app.task_done(number) click.echo(f"{task} marked as done.") @cli.command() @click.argument("number", type=int) @pass_app def remove(app, number): """ Remove task from the list. """ task = app.remove_task(number) click.echo(f"{task} removed from the list.") @cli.command() @click.confirmation_option(prompt="Are you sure you want to remove database") @pass_app def wipe(app): for task in app.list_tasks(): task.remove() 


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 #    , ,  Commands._app NAME todo_fire.py - SYNOPSIS todo_fire.py - GROUP | COMMAND GROUPS GROUP is one of the following: _app Todo Application definition. $ ./todo_fire.py show -- --interactive Fire is starting a Python REPL with the following objects: Modules: atexit, fire, logging, todolib Objects: Commands, args, component, main, result, self, todo_fire.py, trace Python 3.7.4 (default, Aug 15 2019, 13:09:37) [GCC 7.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> self <__main__.Commands object at 0x7fd0a6125bd0> >>> self._app.db {'tasks': [{'title': 'test', 'done': False}]} 


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: # application label label = "todo_cement" # register handlers handlers = [Base] hooks = [("post_setup", lambda app: app.load_db()), ("pre_close", lambda app: app.save())] # call sys.exit() on close close_on_exit = True def main(): with TodoApp() as app: try: app.run() except CaughtSignal as e: if e.signum not in (signal.SIGINT, signal.SIGTERM): raise app.log.debug(f"\n{e}") app.exit_code = 0 


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 #     jinja,       assert app.last_rendered is None 

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 # cleo    clikit,          from clikit.api.io import flags as verbosity class Command(BaseCommand): def __init__(self): super().__init__() self.todoapp = None def handle(self): with todolib.TodoApp.fromenv() as app: self.todoapp = app self.do_handle() def do_handle(self): raise NotImplementedError class AddCommand(Command): """ Add new task. add {task : Task to add} """ def do_handle(self): title = self.argument("task") task = self.todoapp.add_task(title) # will be printed only on "-vvv" self.line(f"Title: {title}", style="comment", verbosity=verbosity.DEBUG) self.line(f"Task <info>{task.title}</> created with number {task.number}.") class ShowCommand(Command): """ Show current tasks. show {--show-done : Include tasks that are done.} """ def do_handle(self): tasks = self.todoapp.list_tasks(self.option("show-done")) if not tasks: self.line("There is no TODOs.", style="info") self.render_table( ["Number", "Title", "Status"], [ [str(task.number), task.title, "" if task.done else "✘"] for task in tasks ], ) class DoneCommand(Command): """ Mark task as done. done {number : Task number} """ def do_handle(self): task = self.todoapp.task_done(int(self.argument("number"))) self.line(f"Task <info>{task.title}</> marked as done.") class RemoveCommand(Command): """ Removes task from the list. remove {number : Task number} """ def do_handle(self): task = self.todoapp.remove_task(int(self.argument("number"))) self.line(f"Task <info>{task.title}</> removed from the list.") 


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.

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


All Articles