Dans l'écosystème Python, il existe de nombreux packages pour les applications CLI, les deux populaires, comme Click, et pas tellement. Les plus courants ont été examinés dans un
article précédent , mais peu connus, mais non moins intéressants, seront présentés ici.

Comme dans la première partie, un script de console pour la bibliothèque todolib sera écrit pour chaque bibliothèque en Python 3.7. En plus de cela, un test trivial avec ces appareils sera écrit pour chaque implémentation:
@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"
Tout le code source est disponible dans
ce référentiel .
falaise
GithubLa documentationBeaucoup ont entendu parler d'OpenStack, une plateforme open source pour IaaS. La majeure partie est écrite en Python, y compris les utilitaires de console qui répètent la fonctionnalité CLI depuis longtemps. Cela a continué jusqu'à ce que cliff, ou le cadre de formulation de l'interface de ligne de commande, apparaisse comme un cadre commun. Avec lui, les développeurs d'Openstack ont combiné des packages comme python-novaclient, python-swiftclient et python-keystoneclient en un
seul programme
openstack .
ÉquipesL'approche de déclaration des commandes ressemble à cement et cleo: argparse en tant qu'analyseur de paramètres, et les commandes elles-mêmes sont créées via l'héritage de la classe Command. Dans le même temps, il existe de petites extensions de la classe Command, telles que Lister, qui formatent indépendamment les données.
code source 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)
Application et principalLa classe d'application a des
méthodes initialize_app et
clean_up , dans notre cas, elles initialisent l'application et enregistrent les données.
code source from cliff import app from cliff.commandmanager import CommandManager from todolib import TodoApp, __version__ class App(app.App): def __init__(self):
Exemples de travaux 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
Déconnexion de la boîte! Et si vous regardez
sous le capot , vous pouvez voir que cela a été fait de manière intelligente: info dans stdout et avertissement / erreur dans stderr et, si nécessaire, est désactivé par le drapeau
--quiet .
igor$ ./todo_cliff.py -q show +--------+----------------------+--------+ | Number | Title | Status | +--------+----------------------+--------+ | 1 | sell the old laptop | ✘ | +--------+----------------------+--------+
Comme déjà mentionné, Lister formate les données, mais le tableau n'est pas limité à:
igor$ ./todo_cliff.py -q show -f json --noindent [{"Number": 0, "Title": "sell old laptop", "Status": "\u2718"}]
En plus de json et table, yaml et csv sont disponibles.
Il existe également une trace cachée par défaut:
igor$ ./todo_cliff.py -q remove 3 No such task.
Un autre REPL disponible et recherche
floue aka
recherche floue :
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
TestC'est simple: un objet App est créé et run () est également appelé, ce qui renvoie un code de sortie.
def test_cliff(capsys): app = todo_cliff.App() code = app.run(["add", "test"]) assert code == 0 out, _ = capsys.readouterr() assert out == EXPECTED
Le pour et le contreAvantages:
- Divers équipements hors de la boîte;
- Développé par OpenStack;
- Mode interactif;
- Extensibilité via setuptools entrypoint et CommandHook;
- Plugin Sphinx pour la documentation automatique à CLI;
- Achèvement de la commande (bash uniquement);
Inconvénients:
- Une petite documentation, qui consiste essentiellement en un exemple détaillé mais unique;
Un autre bug a été remarqué: en cas d'erreur lors du masquage de la trace de pile, le code de sortie est toujours nul.
Plac
GithubLa documentationÀ première vue, Plac semble être quelque chose comme Fire, mais
en réalité, c'est comme Fire, qui cache le même argparse et bien plus sous le capot.
Plac suit, citant la documentation, "l'ancien principe du monde informatique: les
programmes devraient simplement résoudre des cas ordinaires et le simple devrait rester simple, et le complexe en même temps réalisable ". L'auteur du framework utilise Python depuis plus de neuf ans et l'a écrit dans l'espoir de résoudre «99,9% des tâches».
Commandes et principauxAttention aux annotations dans les méthodes show et done: c'est ainsi que Plac analyse respectivement les paramètres et l'aide aux arguments.
code source 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)
TestMalheureusement, le test du code de sortie Plac ne le permet pas. Mais le test lui-même est réel:
def test_plac(capsys): plac.Interpreter.call(todo_plac.TodoInterface, arglist=["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Le pour et le contreAvantages:
- Utilisation simple;
- Mode interactif avec prise en charge de la ligne de lecture;
- API stable
- Excellente documentation;
Mais la chose la plus intéressante à propos de Plac est cachée dans
une utilisation avancée :
- Exécution de plusieurs commandes dans les threads et sous-processus;
- Informatique parallèle;
- serveur telnet;
Inconvénients:
- Vous ne pouvez pas tester le code de sortie;
- Mauvaise durée de vie du projet.
Plumbum
GithubLa documentationDocumentation CLIPlumbum, en fait, n'est pas un cadre aussi peu connu - près de 2000 étoiles, et il y a quelque chose à aimer, car, en gros, il implémente la syntaxe UNIX Shell. Eh bien, avec des additifs:
>>> from plumbum import local >>> output = local["ls"]() >>> output.split("\n")[:3] ['console_examples.egg-info', '__pycache__', 'readme.md']
Commandes et principauxPlumbum a également des outils pour la CLI, et pour ne pas dire qu'ils ne sont qu'un ajout: il y a des nargs, des commandes et des couleurs:
code source 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:
TestLe test des applications sur Plumbum diffère des autres, sauf que la nécessité de transmettre également le nom de l'application, c'est-à-dire premier 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"
Le pour et le contreAvantages:
- Excellente boîte à outils pour travailler avec des équipes externes;
- Prise en charge des styles et des couleurs;
- API stable
- Vie active du projet;
Aucun défaut n'a été remarqué.
cmd2
GithubLa documentationTout d'abord, cmd2 est une extension sur
cmd de la bibliothèque standard,
c'est-à-dire Il est destiné aux applications interactives. Néanmoins, il est inclus dans la revue, car il peut également être configuré pour le mode CLI normal.
Commandes et principauxcmd2 nécessite le respect d'une certaine règle: les commandes doivent commencer par le préfixe
do_ , mais sinon tout est clair:
mode interactif 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()
mode non interactifLe mode normal nécessite un peu de mouvement supplémentaire.
Par exemple, vous devez revenir à argparse et écrire la logique du cas où le script est appelé sans paramètres. Et maintenant, les commandes obtiennent
argparse.Namespace .
L'analyseur est tiré de l'exemple argparse avec de petits ajouts - maintenant les sous-analyseurs sont des attributs du
ArgumentParser principal.
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()
TestSeul un script interactif sera testé.Étant donné que le test d'applications interactives nécessite un grand nombre de ressources humaines et temporelles, les développeurs cmd2 ont essayé de résoudre ce problème à l'aide de
transcriptions - des fichiers texte avec des exemples d'entrée et de sortie attendue. Par exemple:
(Cmd) add test Task 'test' created with number 0.
Ainsi, il suffit de transférer la liste des fichiers avec transcriptions vers l'
App :
def test_cmd2(): todo_cmd2.main(transcript_files=["tests/transcript.txt"])
Le pour et le contreAvantages:
- Bonne API pour les applications interactives;
- Développement actif ces dernières années;
- Une approche originale des tests;
- Bonne documentation.
Inconvénients:
- Nécessite argparse et du code supplémentaire lors de l'écriture d'applications non interactives;
- API instable;
- À certains endroits, la documentation est vide.
Bonus: Urwid
GithubLa documentationTutorielExemples de programmeUrwid est un framework d'un monde légèrement différent - des curses et npyscreen, c'est-à-dire de l'interface utilisateur de la console / du terminal. Néanmoins, il est inclus dans l'examen en tant que bonus, car, à mon avis, il mérite l'attention.
App et équipesUrwid a un grand nombre de widgets, mais il n'a pas de concepts comme une fenêtre ou des outils simples pour accéder aux widgets voisins. Ainsi, si vous voulez obtenir un beau résultat, vous aurez besoin d'une conception réfléchie et / ou de l'utilisation d'autres packages, sinon vous devrez transférer des données dans les attributs des boutons, comme ici:
code source 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",
équipes 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}]"
principal if __name__ == "__main__": try: with app: urwid.MainLoop(app).run() except KeyboardInterrupt: pass
Le pour et le contreAvantages:
- Excellente API pour écrire différentes applications TUI;
- Longue histoire de développement (depuis 2010) et API stable;
- Architecture compétente;
- Bonne documentation, il y a des exemples.
Inconvénients:
- Comment tester n'est pas clair. Seules les clés d'envoi tmux viennent à l'esprit;
- Erreurs non informatives lorsque les widgets ne sont pas correctement organisés.
* * *
Cliff ressemble beaucoup à Cleo and Cement et est généralement bon pour les grands projets.
Personnellement, je n'oserais pas utiliser Plac, mais je recommande de lire le code source.
Plumbum a une boîte à outils CLI pratique et une API brillante pour exécuter d'autres commandes, donc si vous réécrivez des scripts shell en Python, alors c'est ce dont vous avez besoin.
cmd2 est bien adapté comme base pour les applications interactives et pour ceux qui souhaitent migrer à partir du cmd standard.
Et Urwid propose de belles applications de console conviviales.
Les packages suivants n'ont pas été inclus dans l'examen:
- aioconsole - pas de mode non interactif;
- pyCLI - pas de support pour les sous-commandes;
- Clint - pas de support pour les sous-commandes, référentiel dans l'archive;
- ligne de commande - elle est trop ancienne (dernière version en 2009) et sans intérêt;
- CLIArgs - ancien (dernière version en 2010)
- opterator - relativement vieux (dernière version en 2015)