No ecossistema Python, existem muitos pacotes para aplicativos CLI, ambos populares, como Click, e não muito. Os mais comuns foram considerados em um
artigo anterior , mas pouco conhecidos, mas não menos interessantes, serão mostrados aqui.

Como na primeira parte, um script de console para a biblioteca todolib será gravado para cada biblioteca no Python 3.7. Além disso, um teste trivial com esses equipamentos será escrito para cada implementação:
@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 o código-fonte está disponível
neste repositório .
penhasco
GithubA documentaçãoMuitos já ouviram falar do OpenStack, uma plataforma de código aberto para IaaS. A maior parte é escrita em Python, incluindo utilitários de console que repetem a funcionalidade da CLI há muito tempo. Isso continuou até que o penhasco, ou a Estrutura de Formulação da Interface da Linha de Comando, apareceu como uma estrutura comum. Com ele, os desenvolvedores do Openstack combinaram pacotes como python-novaclient, python-swiftclient e python-keystoneclient em um programa
openstack .
EquipasA abordagem para declarar comandos se assemelha a cement e cleo: argparse como um analisador de parâmetros, e os próprios comandos são criados por meio da herança da classe Command. Ao mesmo tempo, existem pequenas extensões para a classe Command, como Lister, que formata os dados independentemente.
código fonte 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)
Aplicação e principalA classe do aplicativo possui os
métodos initialize_app e
clean_up ; no nosso caso, eles inicializam o aplicativo e salvam os dados.
código fonte from cliff import app from cliff.commandmanager import CommandManager from todolib import TodoApp, __version__ class App(app.App): def __init__(self):
Exemplos de trabalho 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
Sair da caixa! E se você olhar por
baixo do capô , poderá ver que isso foi feito de uma maneira inteligente: informações no stdout e aviso / erro no stderr e, se necessário, são desativadas pelo sinalizador
--quiet .
igor$ ./todo_cliff.py -q show +--------+----------------------+--------+ | Number | Title | Status | +--------+----------------------+--------+ | 1 | sell the old laptop | ✘ | +--------+----------------------+--------+
Como já mencionado, o Lister formata os dados, mas a tabela não se limita a:
igor$ ./todo_cliff.py -q show -f json --noindent [{"Number": 0, "Title": "sell old laptop", "Status": "\u2718"}]
Além de json e table, yaml e csv estão disponíveis.
Há também um rastreamento oculto padrão:
igor$ ./todo_cliff.py -q remove 3 No such task.
Outra REPL disponível e pesquisa
difusa, também conhecida como
pesquisa 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
TesteÉ simples: um objeto App é criado e run () também é chamado, que retorna um código de saída.
def test_cliff(capsys): app = todo_cliff.App() code = app.run(["add", "test"]) assert code == 0 out, _ = capsys.readouterr() assert out == EXPECTED
Os prós e contrasPrós:
- Várias comodidades prontas para uso;
- Desenvolvido por OpenStack;
- Modo interativo;
- Extensibilidade através do ponto de entrada setuptools e CommandHook;
- Plugin Sphinx para documentação automática para CLI;
- Conclusão de comandos (somente bash);
Contras:
- Uma pequena documentação, que consiste basicamente em um exemplo detalhado, mas único;
Outro bug foi notado: no caso de um erro ao ocultar o rastreamento da pilha, o código de saída é sempre zero.
Plac
GithubA documentaçãoÀ primeira vista, Plac parece algo como Fogo, mas
na realidade é como Fogo, que esconde o mesmo argumento e muito mais sob o capô.
Plac segue, citando a documentação, "o antigo princípio do mundo da computação: os
programas devem resolver casos comuns e o simples deve permanecer simples, e o complexo ao mesmo tempo possível ". O autor do framework usa o Python há mais de nove anos e o escreveu com a expectativa de resolver "99,9% das tarefas".
Comandos e principaisAtenção às anotações nos métodos show e done: é assim que o Plac analisa os parâmetros e a ajuda do argumento, respectivamente.
código fonte 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)
TesteInfelizmente, o teste do código de saída Plac não permite. Mas o teste em si é real:
def test_plac(capsys): plac.Interpreter.call(todo_plac.TodoInterface, arglist=["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Os prós e contrasPrós:
- Uso simples;
- Modo interativo com suporte a linha de leitura;
- API estável
- Excelente documentação;
Mas a coisa mais interessante sobre o Plac está oculta no
uso avançado :
- Execução de vários comandos em threads e subprocessos;
- Computação paralela;
- servidor de telnet;
Contras:
- Você não pode testar o código de saída;
- Vida de projeto ruim.
Plumbum
GithubA documentaçãoDocumentação CLIO Plumbum, de fato, não é uma estrutura tão pouco conhecida - quase 2000 estrelas, e há algo a amar por isso, porque, grosso modo, implementa a sintaxe do UNIX Shell. Bem, com aditivos:
>>> from plumbum import local >>> output = local["ls"]() >>> output.split("\n")[:3] ['console_examples.egg-info', '__pycache__', 'readme.md']
Comandos e principaisO Plumbum também possui ferramentas para a CLI, para não dizer que são apenas uma adição: existem nargs, comandos e cores:
código fonte 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:
TesteOs aplicativos de teste no Plumbum diferem dos outros, exceto que a necessidade de passar também o nome do aplicativo, ou seja, primeiro 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"
Os prós e contrasPrós:
- Excelente kit de ferramentas para trabalhar com equipes externas;
- Suporte para estilos e cores;
- API estável
- Vida ativa do projeto;
Nenhuma falha foi notada.
cmd2
GithubA documentaçãoPrimeiro, o cmd2 é uma extensão sobre o
cmd da biblioteca padrão,
isto é Destina-se a aplicativos interativos. No entanto, ele está incluído na revisão, pois também pode ser configurado para o modo CLI normal.
Comandos e principaisO cmd2 requer conformidade com uma certa regra: os comandos devem começar com o prefixo
do_ , mas, caso contrário, tudo
ficará claro:
modo interativo 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 não interativoO modo normal requer um pouco de movimento extra.
Por exemplo, você deve retornar ao argparse e escrever a lógica do caso quando o script for chamado sem parâmetros. E agora os comandos obtêm
argparse.Namespace .
O analisador é retirado do exemplo argparse com pequenas adições - agora os sub-analisadores são atributos do
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()
TesteSomente um script interativo será testado.Como o teste de aplicativos interativos requer um grande número de recursos humanos e de tempo, os desenvolvedores do cmd2 tentaram resolver esse problema com a ajuda de
transcrições - arquivos de texto com exemplos de entrada e saída esperada. Por exemplo:
(Cmd) add test Task 'test' created with number 0.
Assim, tudo o que é necessário é transferir a lista de arquivos com transcrições para o
aplicativo :
def test_cmd2(): todo_cmd2.main(transcript_files=["tests/transcript.txt"])
Os prós e contrasPrós:
- Boa API para aplicativos interativos;
- Desenvolvendo ativamente nos últimos anos;
- Uma abordagem original para testes;
- Boa documentação.
Contras:
- Requer código adicional e argparse ao escrever aplicativos não interativos;
- API instável;
- Em alguns lugares, a documentação está vazia.
Bônus: Urwid
GithubA documentaçãoTutorialExemplos de programasUrwid é uma estrutura de um mundo um pouco diferente - de maldições e npyscreen, ou seja, da interface do usuário do console / terminal. No entanto, está incluído na revisão como um bônus, pois, na minha opinião, merece atenção.
Aplicativo e equipesO Urwid possui um grande número de widgets, mas não possui conceitos como uma janela ou ferramentas simples para acessar widgets vizinhos. Portanto, se você deseja obter um resultado bonito, precisará de um design cuidadoso e / ou de outros pacotes, caso contrário, precisará transferir dados nos atributos dos botões, como aqui:
código fonte 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",
equipes 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
Os prós e contrasPrós:
- Ótima API para escrever diferentes aplicativos TUI;
- Longa história de desenvolvimento (desde 2010) e API estável;
- Arquitetura competente;
- Boa documentação, existem exemplos.
Contras:
- Como testar não é claro. Somente as chaves de envio tmux vêm à mente;
- Erros não informativos quando os widgets não estão organizados corretamente.
* * *
Cliff é muito parecido com Cleo e Cement e geralmente é bom para grandes projetos.
Pessoalmente, não ousaria usar o Plac, mas recomendo a leitura do código fonte.
O Plumbum possui um conveniente kit de ferramentas CLI e uma API brilhante para executar outros comandos; portanto, se você estiver reescrevendo scripts de shell no Python, é isso que você precisa.
O cmd2 é adequado como base para aplicativos interativos e para aqueles que desejam migrar do cmd padrão.
O Urwid apresenta aplicativos de console bonitos e fáceis de usar.
Os seguintes pacotes não foram incluídos na revisão:
- aioconsole - sem modo não interativo;
- pyCLI - sem suporte para subcomandos;
- Clint - sem suporte para subcomandos, repositório no arquivo morto;
- linha de comando - é muito antiga (última versão em 2009) e desinteressante;
- CLIArgs - antigo (última versão em 2010)
- opterator - relativamente antigo (última versão em 2015)