Comparaison de bibliothèques CLI moins populaires et peu utilisées: falaise, plac, plumbum et autres (partie 2)

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


Github
La documentation
Beaucoup 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 .

Équipes
L'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) #     '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.") 


Application et principal

La 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): #   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) 


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 

Test

C'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 contre

Avantages:

  • 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


Github
La 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 principaux

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


Test

Malheureusement, 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 contre

Avantages:

  • 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


Github
La documentation
Documentation CLI
Plumbum, 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'] #  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' 

Commandes et principaux

Plumbum 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: # 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() 


Test

Le 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 contre

Avantages:

  • 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


Github
La documentation

Tout 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 principaux

cmd2 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 interactif
Le 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() 


Test

Seul 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 contre

Avantages:

  • 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


Github
La documentation
Tutoriel
Exemples de programme

Urwid 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 équipes

Urwid 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", #       ,  #      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() 


é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}]" #         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) 


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

Le pour et le contre

Avantages:

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

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


All Articles