Comparación de bibliotecas de CLI menos populares y no muy cliff: cliff, plac, plumbum y otras (parte 2)

En el ecosistema de Python, hay muchos paquetes para aplicaciones CLI, ambas populares, como Click, y no tanto. Los más comunes se consideraron en un artículo anterior , pero aquí se mostrarán poco conocidos, pero no menos interesantes.



Como en la primera parte, se escribirá un script de consola para la biblioteca todolib para cada biblioteca en Python 3.7. Además de esto, se escribirá una prueba trivial con estos accesorios para cada implementación:

@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" 

Todo el código fuente está disponible en este repositorio .

acantilado


Github
La documentación
Muchos han escuchado sobre OpenStack, una plataforma de código abierto para IaaS. La mayor parte está escrita en Python, incluidas las utilidades de consola que han estado repitiendo la funcionalidad CLI durante mucho tiempo. Esto continuó hasta que cliff, o el Marco de formulación de la interfaz de línea de comandos, apareció como un marco común. Con él, los desarrolladores de Openstack combinaron paquetes como python-novaclient, python-swiftclient y python-keystoneclient en un programa OpenStack .

Equipos
El enfoque para declarar comandos se asemeja a cement y cleo: argparse como un analizador de parámetros, y los comandos mismos se crean a través de la herencia de la clase Command. Sin embargo, existen pequeñas extensiones para la clase Command, como Lister, que formatea los datos de forma independiente.

código fuente
 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.") 


Aplicación y principal

La clase de aplicación tiene los métodos initialize_app y clean_up , en nuestro caso, inicializan la aplicación y guardan los datos.

código fuente
 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) 


Ejemplos de trabajo

 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 

Saliendo de la caja! Y si mira debajo del capó , puede ver que se hizo de una manera inteligente: información en stdout y advertencia / error en stderr, y, si es necesario, está deshabilitado por el indicador --quiet .

 igor$ ./todo_cliff.py -q show +--------+----------------------+--------+ | Number | Title | Status | +--------+----------------------+--------+ | 1 | sell the old laptop | ✘ | +--------+----------------------+--------+ 

Como ya se mencionó, Lister formatea los datos, pero la tabla no se limita a:

 igor$ ./todo_cliff.py -q show -f json --noindent [{"Number": 0, "Title": "sell old laptop", "Status": "\u2718"}] 

Además de json y table, están disponibles yaml y csv.

También hay un rastro oculto predeterminado:

 igor$ ./todo_cliff.py -q remove 3 No such task. 

Otra REPL disponible y búsqueda difusa también conocida como búsqueda difusa :

 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 

Prueba

Es simple: se crea un objeto App y también se ejecuta run (), que devuelve un código de salida.

 def test_cliff(capsys): app = todo_cliff.App() code = app.run(["add", "test"]) assert code == 0 out, _ = capsys.readouterr() assert out == EXPECTED 

Los pros y contras

Pros:

  • Diversas comodidades fuera de la caja;
  • Desarrollado por OpenStack;
  • Modo interactivo;
  • Extensibilidad a través del punto de entrada setuptools y CommandHook;
  • Complemento Sphinx para documentación automática para CLI;
  • Comando finalizado (solo bash);

Contras:

  • Una pequeña documentación, que básicamente consiste en un ejemplo detallado pero único;

Se notó otro error: en caso de un error al ocultar el seguimiento de la pila, el código de salida siempre es cero.

Plac


Github
La documentación

A primera vista, Plac parece ser algo así como Fire, pero en realidad es como Fire, que esconde el mismo argumento y mucho más bajo el capó.

Plac sigue, citando la documentación, "el antiguo principio del mundo de la informática: los programas deberían resolver casos ordinarios y lo simple debería seguir siendo simple y lo complejo al mismo tiempo posible ". El autor del framework ha estado usando Python por más de nueve años y lo escribió con la expectativa de resolver el "99.9% de las tareas".

Comandos y principales

Atención a las anotaciones en el programa y los métodos realizados: así es como Plac analiza los parámetros y la ayuda de argumentos, respectivamente.

código fuente
 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) 


Prueba

Desafortunadamente, probar el código de salida Plac no permite. Pero la prueba en sí misma es real:

 def test_plac(capsys): plac.Interpreter.call(todo_plac.TodoInterface, arglist=["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED 

Los pros y contras

Pros:

  • Uso simple;
  • Modo interactivo con soporte de línea de lectura;
  • API estable
  • Excelente documentación;

Pero lo más interesante de Plac está oculto en el uso avanzado :

  • Ejecución de varios comandos en subprocesos y subprocesos;
  • Computación paralela;
  • servidor telnet;

Contras:

  • No puede probar el código de salida;
  • Mala vida del proyecto.

Plumbum


Github
La documentación
Documentación de CLI
Plumbum, de hecho, no es un marco tan poco conocido: casi 2000 estrellas, y hay algo que le encanta, porque, en términos generales, implementa la sintaxis de UNIX Shell. Bueno, con aditivos:

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

Comandos y principales

Plumbum también tiene herramientas para la CLI, y no quiere decir que son solo una adición: hay nargs, comandos y colores:

código fuente
 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() 


Prueba

Las aplicaciones de prueba en Plumbum difieren de otras, excepto que la necesidad de pasar también el nombre de la aplicación, es decir primer argumento:

 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" 

Los pros y contras

Pros:

  • Excelente kit de herramientas para trabajar con equipos externos;
  • Soporte para estilos y colores;
  • API estable
  • Vida activa del proyecto;

No se notaron fallas.

cmd2


Github
La documentación

En primer lugar, cmd2 es una extensión sobre cmd de la biblioteca estándar,
es decir Está destinado a aplicaciones interactivas. Sin embargo, se incluye en la revisión, ya que también se puede configurar para el modo CLI normal.

Comandos y principales

cmd2 requiere el cumplimiento de una determinada regla: los comandos deben comenzar con el prefijo do_ , pero de lo contrario todo está claro:

modo interactivo
 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() 


modo no interactivo
El modo normal requiere un poco de movimiento extra.

Por ejemplo, debe volver a argparse y escribir la lógica del caso cuando se llama al script sin parámetros. Y ahora los comandos obtienen argparse.Namespace .

El analizador se toma del ejemplo argparse con pequeñas adiciones; ahora los analizadores secundarios son atributos del 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() 


Prueba

Solo se probará un script interactivo.

Dado que probar aplicaciones interactivas requiere una gran cantidad de recursos humanos y de tiempo, los desarrolladores de cmd2 intentaron resolver este problema con la ayuda de transcripciones : archivos de texto con ejemplos de entrada y salida esperada. Por ejemplo:

 (Cmd) add test Task 'test' created with number 0. 

Por lo tanto, todo lo que se requiere es transferir la lista de archivos con transcripciones a la aplicación :

 def test_cmd2(): todo_cmd2.main(transcript_files=["tests/transcript.txt"]) 

Los pros y contras

Pros:

  • Buena API para aplicaciones interactivas;
  • Desarrollando activamente en los últimos años;
  • Un enfoque original para las pruebas;
  • Buena documentación

Contras:

  • Requiere argparse y código adicional al escribir aplicaciones no interactivas;
  • API inestable;
  • En algunos lugares la documentación está vacía.

Bonus: Urwid


Github
La documentación
Tutorial
Ejemplos de programa

Urwid es un marco de un mundo ligeramente diferente: desde maldiciones y npyscreen, es decir, desde la consola / terminal de la interfaz de usuario. Sin embargo, se incluye en la revisión como un bono, ya que, en mi opinión, merece atención.

App y equipos

Urwid tiene una gran cantidad de widgets, pero no tiene conceptos como una ventana o herramientas simples para acceder a widgets vecinos. Por lo tanto, si desea obtener un resultado hermoso, necesitará un diseño cuidadoso y / o el uso de otros paquetes, de lo contrario tendrá que transferir datos en los atributos de los botones, como aquí:

código fuente
 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() 


equipos
 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 

Los pros y contras

Pros:

  • Gran API para escribir diferentes aplicaciones TUI;
  • Largo historial de desarrollo (desde 2010) y API estable;
  • Arquitectura competente;
  • Buena documentación, hay ejemplos.

Contras:

  • La forma de probar no está clara. Solo me vienen a la mente tmux send-keys ;
  • Errores no informativos cuando los widgets no están organizados correctamente.

* * *


Cliff se parece mucho a Cleo y Cement y generalmente es bueno para grandes proyectos.

Personalmente no me atrevería a usar Plac, pero recomiendo leer el código fuente.

Plumbum tiene un práctico kit de herramientas CLI y una API brillante para ejecutar otros comandos, por lo que si está reescribiendo scripts de shell en Python, entonces esto es lo que necesita.
cmd2 es muy adecuado como base para aplicaciones interactivas y para aquellos que desean migrar desde el cmd estándar.

Y Urwid presenta aplicaciones de consola hermosas y fáciles de usar.

Los siguientes paquetes no se incluyeron en la revisión:

  • aioconsola : sin modo no interactivo;
  • pyCLI : no se admiten subcomandos;
  • Clint : no hay soporte para subcomandos, repositorio en el archivo;
  • línea de comandos : es demasiado antigua (última versión en 2009) y no es interesante;
  • CLIArgs - antiguo (última versión en 2010)
  • operador - relativamente antiguo (último lanzamiento en 2015)

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


All Articles