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
GithubLa documentaciónMuchos 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 .
EquiposEl 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)
Aplicación y principalLa 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):
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
PruebaEs 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 contrasPros:
- 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
GithubLa documentaciónA 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 principalesAtenció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)
PruebaDesafortunadamente, 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 contrasPros:
- 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
GithubLa documentaciónDocumentación de CLIPlumbum, 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']
Comandos y principalesPlumbum 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:
PruebaLas 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 contrasPros:
- 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
GithubLa documentaciónEn 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 principalescmd2 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 interactivoEl 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()
PruebaSolo 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 contrasPros:
- 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
GithubLa documentaciónTutorialEjemplos de programaUrwid 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 equiposUrwid 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",
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}]"
principal if __name__ == "__main__": try: with app: urwid.MainLoop(app).run() except KeyboardInterrupt: pass
Los pros y contrasPros:
- 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)