
O Python é uma ótima linguagem para aplicativos de console e destaca um grande número de bibliotecas para essas tarefas.
Mas quais bibliotecas existem? E qual é o melhor para levar? Este material compara ferramentas populares e não muito para o mundo dos consoles e tenta responder à segunda pergunta.
Para facilitar a leitura, a revisão é dividida em duas postagens: a primeira compara as seis bibliotecas mais populares, a segunda - a menos popular e mais específica, mas ainda digna de atenção.
Em cada exemplo, um utilitário de console para a biblioteca
todolib será gravado no Python 3.7, com o qual você pode criar, exibir, marcar e excluir tarefas. O restante será adicionado, sujeito à simplicidade da implementação em uma estrutura específica. As próprias tarefas são armazenadas em um arquivo json, que será salvo em uma chamada separada - uma condição adicional para os exemplos.
Além disso, um teste trivial será gravado para cada implementação. O Pytest com os seguintes equipamentos foi usado como uma estrutura de teste:
@pytest.fixture(autouse=True) def db(monkeypatch): """ monkeypatch , """ value = {"tasks": []} 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"
Em princípio, todas as opções acima serão suficientes para demonstrar as bibliotecas. O código fonte completo está disponível
neste repositório.
argparse
Argparse tem uma vantagem inegável - está na biblioteca padrão e sua API não é difícil de aprender: existe um analisador, há argumentos, os argumentos têm
tipo ,
ação ,
dest ,
padrão e
ajuda . E há
subparser - a capacidade de separar parte dos argumentos e da lógica em comandos separados.
Analisador
À primeira vista - nada de anormal, o analisador é como um analisador. Mas - na minha opinião - a legibilidade não é a melhor quando comparada com outras bibliotecas, porque argumentos para diferentes comandos são descritos em um só lugar.
código fonte def get_parser(): parser = argparse.ArgumentParser("Todo notes - argparse version") parser.add_argument( "--verbose", "-v", action="store_true", help="Enable verbose mode" ) parser.add_argument("--version", "-V", action="store_true", help="Show version") subparsers = parser.add_subparsers(title="Commands", dest="cmd") add = subparsers.add_parser("add", help="Add new task") add.add_argument("title", help="Todo title") show = subparsers.add_parser("show", help="Show tasks") show.add_argument( "--show-done", action="store_true", help="Include done tasks in the output" ) done = subparsers.add_parser("done", help="Mark task as done") done.add_argument("number", type=int, help="Task number") remove = subparsers.add_parser("remove", help="Remove task") remove.add_argument("number", type=int, help="Task number") return parser
principal
E aqui a mesma coisa - o analisador, exceto os argumentos de análise, não pode fazer mais nada; portanto, a lógica terá que ser escrita independentemente e em um só lugar. Por um lado - é possível viver, por outro - é possível melhor, mas ainda não está claro como.
UPD: Como foldr observou, de fato, os subparsers podem definir funções via set_defaults (func = foo), ou seja, argparse permite reduzir o tamanho principal para o tamanho pequeno. Viva e aprenda.código fonte def main(raw_args=None): """ Argparse example entrypoint """ parser = get_parser() args = parser.parse_args(raw_args) logging.basicConfig() if args.verbose: logging.getLogger("todolib").setLevel(logging.INFO) if args.version: print(lib_version) exit(0) cmd = args.cmd if not cmd: parser.print_help() exit(1) with TodoApp.fromenv() as app: if cmd == "add": task = app.add_task(args.title) print(task, "created with number", task.number, end=".\n") elif cmd == "show": app.print_tasks(args.show_done) elif cmd == "done": task = app.task_done(args.number) print(task, "marked as done.") elif cmd == "remove": task = app.remove_task(args.number) print(task, "removed from list.")
Teste
Para verificar a saída do utilitário, é
usado o acessório
capsys , que fornece acesso ao texto de stdout e stderr.
def test_argparse(capsys): todo_argparse.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Sumário
Das vantagens - um bom conjunto de recursos para análise, a presença de um módulo na biblioteca padrão.
Em contrapartida, a argparse está envolvida apenas na análise de argumentos, a maior parte da lógica principal teve que ser escrita por mim. E não está claro como testar o código de saída nos testes.
docopt
O docopt é um
analisador pequeno (<600 linhas, comparado a 2500 com argparse), que fará você sorrir, citando uma descrição no GitHub. A idéia principal do docopt é descrever a interface literalmente com texto, por exemplo, na sequência de documentos.
No mesmo github, docopt> 6700 estrelas, é usado em pelo menos 22 mil outros projetos. E isso é apenas com a implementação do python! A página do projeto docopt possui muitas opções para diferentes linguagens, de C e PHP a CoffeeScript e até R. Essa plataforma cruzada só pode ser explicada pela compacidade e simplicidade do código.
Analisador
Comparado ao argparse, esse analisador é um grande passo à frente.
"""Todo notes on docopt. Usage: todo_docopt [-v | -vv ] add <task> todo_docopt [-v | -vv ] show --show-done todo_docopt [-v | -vv ] done <number> todo_docopt [-v | -vv ] remove <number> todo_docopt -h | --help todo_docopt --version Options: -h --help Show help. -v --verbose Enable verbose mode. """
principal
Em geral, tudo é o mesmo que com argparse, mas agora
detalhado pode ter vários valores (0-2), e o acesso aos argumentos é diferente: docopt não retorna um espaço para nome com atributos, mas apenas um dicionário, onde a escolha de um comando é indicada através de seu booleano, como visto
se :
código fonte def main(argv=None): args = docopt(__doc__, argv=argv, version=lib_version) log.setLevel(levels[args["--verbose"]]) logging.basicConfig() log.debug("Arguments: %s", args) with TodoApp.fromenv() as app: if args["add"]: task = app.add_task(args["<task>"]) print(task, "created with number", task.number, end=".\n") elif args["show"]: app.print_tasks(args["--show-done"]) elif args["done"]: task = app.task_done(args["<number>"]) print(task, "marked as done.") elif args["remove"]: task = app.remove_task(args["<number>"]) print(task, "removed from list.")
Teste
Semelhante ao teste argparse:
def test_docopt(capsys): todo_docopt.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
Sumário
Dos benefícios - muito menos código para o analisador, facilidade de descrição e leitura de comandos e argumentos, versão embutida.
Contras, em primeiro lugar, o mesmo que argparse - muita lógica em
main , você não pode testar o código de saída. Além disso, a versão atual (0.6.2) do docopt ainda não é estável e provavelmente nunca será - o projeto foi desenvolvido
ativamente de 2012 até o final de 2013, o último commit foi em 17 de dezembro. E a coisa mais desagradável no momento é que alguns docopt regulares provocam DeprecationWarning ao executar testes.
Clique em
O Click é fundamentalmente diferente de argparse e docopt pelo número de recursos e pela abordagem para descrever comandos e parâmetros por meio de decoradores, e propõe-se que a própria lógica seja separada em funções separadas, em vez de uma grande. Os autores afirmam que o Click possui muitas configurações, mas os parâmetros padrão devem ser suficientes. Entre os recursos, os comandos aninhados e seu carregamento lento são enfatizados.
O projeto é extremamente popular: além de ter mais de 8100 estrelas e usá-lo em pelo menos 174 mil projetos (!), Ele ainda está em desenvolvimento: a versão 7.0 foi lançada no outono de 2018 e novas solicitações de confirmação e mesclagem aparecem até hoje. dia
Analisador
Na página de documentação, encontrei o decorador de
confirmação_opção , que solicita confirmação do usuário antes de executar o comando. Para demonstrá-lo, o comando wipe foi adicionado, o que limpa toda a lista de tarefas.
código fonte levels = [logging.WARN, logging.INFO, logging.DEBUG] pass_app = click.make_pass_decorator(TodoApp) @click.group() @click.version_option(lib_version, prog_name="todo_click") @click.option("-v", "--verbose", count=True)
principal
E aqui encontramos a principal vantagem do Click - devido ao fato de a lógica dos comandos ser separada por suas funções, quase nada permanece principal. Também é demonstrada aqui a capacidade da biblioteca de receber argumentos e parâmetros de variáveis de ambiente.
if __name__ == "__main__": cli(auto_envvar_prefix="TODO")
Teste
No caso do Click, não há necessidade de interceptar o sys.stdout, pois existe um módulo de teste de
click.t com um corredor para essas coisas. E
o CliRunner não apenas intercepta a saída, mas também permite verificar o código de saída, o que também é legal. Tudo isso permite testar os utilitários de clique sem usar o pytest e ignorar o módulo
unittest padrão.
import click.testing def test_click(): runner = click.testing.CliRunner() result = runner.invoke(todo_click.cli, ["add", "test"]) assert result.exit_code == 0 assert result.output == EXPECTED
Sumário
Esta é apenas uma pequena parte do que o Click pode fazer. Do restante da API - validação de valores, integração com o terminal (cores, pager a la less, barra de progresso etc.), retorno de chamada de resultado, preenchimento automático e muito mais. Você pode ver os exemplos
aqui .
Prós: muitas ferramentas para qualquer ocasião, uma abordagem original, mas ao mesmo tempo conveniente para descrever equipes, facilidade de teste e vida ativa do projeto.
Contras: Quais são as desvantagens de um "clique" - essa é uma pergunta difícil. Talvez ele não saiba algo do que as seguintes bibliotecas são capazes?
Fogo
O Fire não é apenas uma biblioteca jovem (publicada em 2017) para interfaces de console do Google, é uma biblioteca para gerar interfaces de console de, entre aspas,
absolutamente qualquer objeto Python.
Entre outras coisas, afirma-se que o fire ajuda no desenvolvimento e na depuração de código, ajuda a adaptar o código existente na CLI, facilita a transição do bash para o Python e tem seu próprio REPL para trabalho interativo. Vamos ver?
Analisador e principal
O fire.Fire é realmente capaz de aceitar qualquer objeto: um módulo, uma instância de classe, um dicionário com nomes de comandos e funções correspondentes, e assim por diante.
O que é importante para nós é que o Fire permite a transferência de um objeto de classe. Assim, o construtor da classe aceita argumentos comuns a todos os comandos, e seus métodos e atributos são comandos separados. Vamos usar isso:
código fonte class Commands: def __init__(self, db=None, verbose=False): level = logging.INFO if verbose else logging.WARNING logging.basicConfig(level=level) logging.getLogger("todolib").setLevel(level) self._app = todolib.TodoApp.fromenv(db) atexit.register(self._app.save) def version(self): return todolib.__version__ def add(self, task): """Add new task.""" task = self._app.add_task(task) print(task, "created with number", task.number, end=".\n") def show(self, show_done=False): """ Show current tasks. """ self._app.print_tasks(show_done) def done(self, number): """ Mark task as done. """ task = self._app.task_done(number) print(task, "marked as done.") def remove(self, number): """ Removes task from the list. """ task = self._app.remove_task(number) print(task, "removed from the list.") def main(args=None): fire.Fire(Commands, command=args)
Sinalizadores embutidos
O Fire possui suas próprias bandeiras com uma sintaxe especial (elas devem ser passadas após o "-"), que permitem que você olhe sob o capô do analisador e do aplicativo como um todo:
exemplos de chamadas $ ./todo_fire.py show -- --trace Fire trace: 1. Initial component 2. Instantiated class "Commands" (todo_fire.py:9) 3. Accessed property "show" (todo_fire.py:25) $ ./todo_fire.py -- --verbose | head -n 12
Teste
Testar a função principal é semelhante a testar argparse e docopt, então não vejo o ponto aqui.
Ao mesmo tempo, vale a pena notar que, devido à natureza introspectiva do fogo, é igualmente possível testar a classe Commands imediatamente.
Sumário
O fogo é uma ferramenta não menos interessante que o clique. Não é necessário listar muitas opções no analisador, a configuração é mínima, há opções para depuração e a própria biblioteca
vive e se desenvolve ainda mais ativamente do que o clique (60 confirma neste verão).
Contras: pode ser significativamente menor que o clique e outros analisadores; API instável (a versão atual é 0.2.1).
Cimento
De fato,
Cement não
é exatamente uma biblioteca CLI, mas uma estrutura para aplicativos de console, mas argumenta-se que é adequado para scripts e aplicativos complexos com várias integrações.
Analisador
O analisador em Cement parece incomum, mas se você observar atentamente os parâmetros, é fácil adivinhar que o argumento familiar está sob o capô. Mas talvez isso seja o melhor - não há necessidade de aprender novos parâmetros.
código fonte from cement import Controller, ex class Base(Controller): class Meta: label = "base" arguments = [ ( ["-v", "--version"], {"action": "version", "version": f"todo_cement v{todolib.__version__}"}, ) ] def _default(self): """Default action if no sub-command is passed.""" self.app.args.print_help() @ex(help="Add new task", arguments=[(["task"], {"help": "Task title"})]) def add(self): title = self.app.pargs.task self.app.log.debug(f"Task title: {title!r}") task = self.app.todoobj.add_task(title) print(task, "created with number", task.number, end=".\n") @ex( help="Show current tasks", arguments=[ (["--show-done"], dict(action="store_true", help="Include done tasks")) ], ) def show(self): self.app.todoobj.print_tasks(self.app.pargs.show_done) @ex(help="Mark task as done", arguments=[(["number"], {"type": int})]) def done(self): task = self.app.todoobj.task_done(self.app.pargs.number) print(task, "marked as done.") @ex(help="Remove task from the list", arguments=[(["number"], {"type": int})]) def remove(self): task = self.app.todoobj.remove_task(self.app.pargs.number) print(task, "removed from the list.")
App e principal
O cimento, entre outras coisas, ainda envolve sinais em exceções. Isso é demonstrado aqui na saída de código zero com SIGINT / SIGTERM.
código fonte class TodoApp(App): def __init__(self, argv=None): super().__init__(argv=argv) self.todoobj = None def load_db(self): self.todoobj = todolib.TodoApp.fromenv() def save(self): self.todoobj.save() class Meta:
Se você entender o main, poderá ver que o carregamento e o salvamento do todolib.TodoApp também podem ser feitos no __enter __ / __ exit__ substituído, mas essas fases acabaram sendo separadas em métodos separados para demonstrar ganchos de cimento.
Teste
Para teste, você pode usar a mesma classe de aplicativo:
def test_cement(capsys): with todo_cement.TodoApp(argv=["add", "test"]) as app: app.run() out, _ = capsys.readouterr() assert out == EXPECTED
Sumário
Prós: O conjunto de APIs é como um conjunto de facas suíças, extensibilidade através de ganchos e plugins, uma interface estável e desenvolvimento ativo.
Contras: Em locais com documentação vazia; pequenos scripts baseados em cimento podem parecer um pouco complicados.
Cleo
O Cleo está longe de ser uma estrutura tão popular quanto as outras listadas aqui (cerca de 400 estrelas no GitHub no total), e ainda assim consegui conhecê-la quando estudei como o formato Poetry foi formatado.
Portanto, o Cleo é um dos projetos do autor da já mencionada Poesia, uma ferramenta para gerenciar dependências, virtualenvs e compilações de aplicativos. Sobre a poesia em um habr já mais de uma vez escreveu, e sobre sua parte do console - não.
Analisador
Cleo, como Cimento, é construído sobre princípios de objetos, ou seja, Os comandos são definidos por meio da classe Command e sua documentação, os parâmetros são acessados pelo método option () e assim por diante. Além disso, o método line (), usado para gerar texto, suporta estilos (ou seja, cores) e filtragem de saída com base no número de sinalizadores detalhados da caixa. Cleo também tem saída de tabela. E também barras de progresso. E ainda ... Em geral, veja:
código fonte from cleo import Command as BaseCommand
principal
Tudo o que é necessário é criar um objeto
cleo.Application e passar comandos para add_commands. Para não repetir durante o teste, tudo isso foi transferido do principal para o construtor:
from cleo import Application as BaseApplication class TodoApp(BaseApplication): def __init__(self): super().__init__(name="ToDo app - cleo version", version=todolib.__version__) self.add_commands(AddCommand(), ShowCommand(), DoneCommand(), RemoveCommand()) def main(args=None): TodoApp().run(args=args)
Teste
Para testar comandos no Cleo, existe o
CommandTester , que, como todos os
tios adultos
da estrutura, intercepta E / S e código de saída:
def test_cleo(): app = todo_cleo.TodoApp() command = app.find("add") tester = cleo.CommandTester(command) tester.execute("test") assert tester.status_code == 0 assert tester.io.fetch_output() == "Task test created with number 0.\n"
Sumário
Prós: estrutura de objetos com dicas de tipo, o que simplifica o desenvolvimento (como muitos IDEs e editores têm bom suporte para código OOP e módulo de digitação); Uma boa quantidade de funcionalidade para trabalhar não apenas com argumentos, mas também com E / S.
Mais ou menos: seu parâmetro de verbosidade, compatível apenas com a E / S Cleo / CliKit. Embora você possa escrever um manipulador personalizado para o módulo de registro em log, pode ser difícil manter junto com o desenvolvimento do cleo.
Contras: obviamente - uma opinião pessoal - uma API jovem: o framework não possui outro usuário "grande", exceto Poetry, e o Cleo está desenvolvendo paralelamente ao desenvolvimento e às necessidades de um; Às vezes, a documentação está desatualizada (por exemplo, os níveis de log agora não estão no módulo clikit, mas em clikit.api.io.flags) e, em geral, é ruim e não reflete a API inteira.
Cleo, comparado com Cement, está mais focado na CLI e é o único que pensou em formatar (ocultar o rastreamento de pilha padrão) de exceções na saída padrão. Mas ele - novamente uma opinião pessoal - perde para Cement na juventude e na estabilidade da API.
Em conclusão
Neste ponto, todos já têm sua própria opinião, o que é melhor, mas a conclusão deve ser: Gostei mais do Click, porque há muitas coisas nele e é muito fácil desenvolver e testar aplicativos com ele. Se você tentar escrever código no mínimo - comece com o Fire. Seu script precisa acessar o Memcached, formatar com jinja e extensibilidade - use Cement e você não se arrependerá. Você tem um projeto para animais de estimação ou deseja tentar outra coisa - veja o cleo.