Von Click zurück zu argparse
Um meinen Python-Programmen ein Kommandozeileninterface zu geben, habe ich in letzter Zeit Click genutzt. Das ist als Ersatz für das Modul argparse aus der Python-Standardbibliothek gedacht. Zuerst war es mir suspekt, weil man es mit Dekoratoren baut. Aber mit der Zeit fand ich es ziemlich elegant. So sieht das hier für die Skripte mit denen ich meinen Blog verwalte aus:
@click.group() @click.option( "--loglevel", type=click.Choice(["debug", "info", "warning", "error", "critical"]), default="info", show_default=True, help="Controls the verbosity of logging.", ) def main(loglevel: str): """ Utilities for my Nikola blog. """ coloredlogs.install(level=loglevel.upper()) @main.command() @click.option("--fix/--no-fix", default=True, help="Apply automatic fixes.", show_default=True) @click.option("--validate/--no-validate", default=True, help="Perform validation of post meta-data.", show_default=True) @click.option("--publish/--no-publish", default=False, help="Publish an automatically determined amount of posts today.", show_default=True) @click.option("--build/--no-build", default=True, help="Build the blog with Nikola.", show_default=True) @click.option("--upload/--no-upload", default=False, help="Upload the generated files to the live server.", show_default=True) def make(fix: bool, validate: bool, publish: bool, build: bool, upload: bool) -> None: """ Build the website and other auxiliary files. There are a couple of stages which can be enabled or skipped with the additional flags. """ # … @main.command() def gui(): """ Start the GUI with Kanban board. """ from .qtgui import main main() if __name__ == "__main__": main()
Schaut man sich die Hilfe zum Haupt-Kommando an, dann bekommt man das hier:
❯ blog --help Usage: blog [OPTIONS] COMMAND [ARGS]... Utilities for my Nikola blog. Options: --loglevel [debug|info|warning|error|critical] Controls the verbosity of logging. [default: info] --help Show this message and exit. Commands: append-images auto-move gui Start the GUI with Kanban board. make Build the website and other auxiliary files. move new shrink-images Shrink images in the `raw` directory to just 2 megapixel... tag-cloud
Aus den Docstrings wird automatisch die Hilfe für das Hauptkommando extrahiert, sowie für die Unterkommandos die Zusammenfassung. Man muss also relativ wenig machen und hat schon eine vernünftige Hilfe.
Auch bei den Unterkommandos bekommt man eine Hilfe. Hier wird dann der komplette Docstring angezeigt.
❯ blog make --help Usage: blog make [OPTIONS] Build the website and other auxiliary files. There are a couple of stages which can be enabled or skipped with the additional flags. Options: --fix / --no-fix Apply automatic fixes. [default: fix] --validate / --no-validate Perform validation of post meta-data. [default: validate] --publish / --no-publish Publish an automatically determined amount of posts today. [default: no-publish] --build / --no-build Build the blog with Nikola. [default: build] --upload / --no-upload Upload the generated files to the live server. [default: no-upload] --help Show this message and exit.
Das ist alles ziemlich hübsch aufbereitet. Auch toll sind die Flag-Kombinationen aus --fix
und --no-fix
, mit denen man umschalten kann.
Praktisch ist, dass man nur ein paar Dekoratoren nutzen muss, und schon hat man ein Interface mit mehreren Unterkommandos. Auch die Docstrings werden entsprechend verteilt. Das unschöne daran ist allerdings, dass das Kommandozeileninterface damit dann aber so verteilt ist. Man muss sich hier wohl entscheiden, was man zusammengruppieren möchte: Soll das Kommandozeileninterface in einem zusammengepackt sein, oder will man lieber den Code und das Interface stärker zusammen binden?
Das gleiche mit argparse
Generell stört mich an Click, dass es einfach eine Abhängigkeit mehr ist. Und dadurch ergeben sich dann mehr Versionskonflikte, als nötig. Steamlit nutzt auch Click, und zwar meist in einer etwas älteren Version. Damit war ich dann auf Click 7 festgenagelt, obwohl Click 8 auch schon erschienen war. Ich versuche eigentlich solange mit der Standardbibliothek zu arbeiten, solange es geht. Und eigentlich ist argparse
in Python auch ziemlich ordentlich.
Hier ist das gleiche mit argparse
implementiert. Da haben wir alle Unterkommandos in einem definiert, man hat das ganze Interface in einer Funktion gesammelt.
def main() -> None: parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Utilities for my Nikola blog.' ) parser.add_argument( "--loglevel", choices=["debug", "info", "warning", "error", "critical"], default="info", ) parser.set_defaults(func=None) sub_parsers = parser.add_subparsers(help="Sub-commands") new_parser = sub_parsers.add_parser("new", help="Create a new blog entry.") new_parser.add_argument("title") new_parser.set_defaults(func=lambda options: templates.make_post(options.title)) make_parser = sub_parsers.add_parser( "make", help="Build the website and other auxiliary files." ) make_parser.add_argument( "--fix", default=True, action=argparse.BooleanOptionalAction, help="Apply automatic fixes.", ) make_parser.add_argument( "--validate", default=True, action=argparse.BooleanOptionalAction, help="Perform validation of post meta-data.", ) make_parser.add_argument( "--publish", default=False, action=argparse.BooleanOptionalAction, help="Publish an automatically determined amount of posts today.", ) make_parser.add_argument( "--build", default=True, action=argparse.BooleanOptionalAction, help="Build the blog with Nikola.", ) make_parser.add_argument( "--upload", default=False, action=argparse.BooleanOptionalAction, help="Upload the generated files to the live server.", ) make_parser.set_defaults( func=lambda options: make( options.fix, options.validate, options.publish, options.build, options.upload, ) ) move_parser = sub_parsers.add_parser("move", help="Rename a blog post.") move_parser.add_argument("source") move_parser.add_argument("destination") move_parser.set_defaults( func=lambda options: moving.move(options.source, options.destination) ) auto_move_parser = sub_parsers.add_parser( "auto-move", help="Automatically move blog posts to match their titles." ) auto_move_parser.set_defaults(func=lambda options: auto_move()) append_images_parser = sub_parsers.add_parser( "append-images", help="Append given images to a blog post." ) append_images_parser.add_argument("post") append_images_parser.add_argument("images", metavar="image", nargs=-1) append_images_parser.set_defaults( func=lambda options: append_images(options.post, options.images) ) shrink_images_parser = sub_parsers.add_parser( "shrink-images", help="Shrink images in the `raw` directory." ) shrink_images_parser.set_defaults(func=lambda options: shrink_images()) gui_parser = sub_parsers.add_parser("gui", help="Start the Kanban board GUI.") gui_parser.set_defaults(func=lambda options: gui()) options = parser.parse_args() coloredlogs.install(level=options.loglevel.upper()) options.func(options)
Der restliche Code weiß jetzt nichts mehr vom Interface. Das kann besser sein, das kann aber auch schlechter sein. Meine Hoffnung in Click war, dass man damit wirklich auch Unterkommandos in anderen Dateien definieren kann. Das klappt aber nicht so wirklich. Mit Argparse wäre das sogar noch eher möglich, man könnte den Sub-Parser in einer anderen Datei zusammenbauen und das Lambda für den Aufruf dort einhängen. An sich wäre das also sogar noch flexibler, glaube ich.
Die Ausgabe ist allerdings nicht ganz so hübsch, wie bei Click:
❯ poetry run blog --help usage: blog [-h] [--loglevel {debug,info,warning,error,critical}] {new,make,move,auto-move,append-images,shrink-images,gui} ... Utilities for my Nikola blog. positional arguments: {new,make,move,auto-move,append-images,shrink-images,gui} Sub-commands new Create a new blog entry. make Build the website and other auxiliary files. move Rename a blog post. auto-move Automatically move blog posts to match their titles. append-images Append given images to a blog post. shrink-images Shrink images in the `raw` directory. gui Start the Kanban board GUI. options: -h, --help show this help message and exit --loglevel {debug,info,warning,error,critical}
Und für das Unterkommando:
❯ poetry run blog make --help usage: blog make [-h] [--fix | --no-fix] [--validate | --no-validate] [--publish | --no-publish] [--build | --no-build] [--upload | --no-upload] options: -h, --help show this help message and exit --fix, --no-fix Apply automatic fixes. (default: True) --validate, --no-validate Perform validation of post meta-data. (default: True) --publish, --no-publish Publish an automatically determined amount of posts today. (default: False) --build, --no-build Build the blog with Nikola. (default: True) --upload, --no-upload Upload the generated files to the live server. (default: False)
So richtig ersichtlich wird das mit den Unterkommandos bei Argparse nicht, das hat Click deutlich besser gemacht. Das kann man bei Argparse im Prinzip auch mit angepassten Formatierern ändern, bei Click ist das einfach nicht nötig.
Ob einem das jetzt eine zusätzliche Abhängigkeit wert ist, muss man selbst entscheiden. Ich denke, ich werde versuchen möglichst darauf zu verzichten, um die Abhängigkeiten möglichst schlank zu halten.