比较流行的Python Python CLI库:click,conce,fire和其他



Python是控制台应用程序的一种出色语言,它突出显示了用于这些任务的大量库。 但是,存在哪些库? 哪个更好? 该材料比较了控制台世界中流行的工具,但不是很好,并尝试回答第二个问题。

为了便于阅读,该评论分为两篇文章:第一篇比较了六个最受欢迎的图书馆,第二篇比较不受欢迎,但更具体,但仍然值得关注。

在每个示例中,将使用Python 3.7编写todolib库的控制台实用程序,您可以使用该实用程序创建,查看,标记和删除任务。 其余的将在特定框架上简化实施的前提下添加。 任务本身存储在json文件中,该文件将保存在单独的调用中-这些示例的附加条件。
除此之外,将为每个实现编写一个简单的测试。 带有以下固定装置的Pytest被用作测试框架:

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

原则上,以上所有内容都足以说明这些库。 完整的源代码在存储库中可用。

argparse


Argparse具有不可否认的优势-它在标准库中,其API也不难学习:有一个解析器,有参数,这些参数具有typeactiondestdefaulthelp 。 并具有子解析器 -能够将部分参数和逻辑分离为单独的命令。

解析器


乍一看-没什么异常,解析器就像解析器。 但是-我认为-与其他库相比,可读性不是最好的,因为 不同命令的参数在一个地方描述。

源代码
 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 


主要的


而且这里是同一件事-解析器除了解析参数外无能为力,因此逻辑将必须独立编写并放在一个位置。 一方面-有可能生存,另一方面-可能会更好,但目前尚不清楚。

UPD: 正如foldr所述,实际上,子解析器可以通过set_defaults(func = foo)设置函数,即argparse允许您将main缩短为小尺寸。 生活和学习。

源代码
 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.") 


测试中


要检查该实用程序的输出,使用了capsys固定装置,该装置可以访问stdout和stderr中的文本。

 def test_argparse(capsys): todo_argparse.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED 

总结


优点-一组很好的解析功能,标准库中存在模块。

缺点-argparse仅参与参数解析,main中的大多数逻辑必须由我自己编写。 尚不清楚如何在测试中测试退出代码。

docopt


docopt是一个小型解析器(小于600行,而使用argparse则为2500行)解析器,引用GitHub上的描述,会让您微笑。 docopt的主要思想是使用文本(例如,在docstring中)以文字形式描述接口。

在同一个github,docopt> 6700个stars上,它至少在22,000个其他项目中使用。 而且这仅适用于python实现! docopt项目页面具有用于不同语言的许多选项,从C和PHP到CoffeeScript甚至R。这种跨平台只能由代码的紧凑性和简单性来解释。

解析器


与argparse相比,此解析器向前迈出了一大步。

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

主要的


通常,所有内容都与argparse相同,但是现在verbose可以具有多个值(0-2),并且对参数的访问有所不同:docopt返回的不是具有属性的名称空间,而只是返回字典,在其中指定了命令的选择通过她的布尔值,如if所示

源代码
 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.") 


测试中


与argparse测试类似:
 def test_docopt(capsys): todo_docopt.main(["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED 

总结


好处包括-解析器的代码更少,易于描述以及命令和参数的读取,内置版本。

缺点,首先,和argparse一样-main中有很多逻辑,您不能测试退出代码。 此外,当前的docopt版本(0.6.2)尚不稳定,而且不太可能实现-该项目从2012年到2013年底一直在积极开发,最后一次提交是在12月17日。 目前最不愉快的是,某些docopt常规在执行测试时会引发DeprecationWarning。

请点击


Click从本质上与argparse和docopt的不同之处在于功能的数量以及通过装饰器描述命令和参数的方法,并且逻辑本身被建议分离为单独的功能,而不是大型main 。 作者声称Click有很多设置,但是标准参数应该足够了。 在这些功能中,强调了嵌套命令及其延迟加载。

该项目非常受欢迎:除了拥有超过8100颗星并在至少17.4万个(!)项目中使用它之外,它仍在开发中:7.0版已于2018年秋季发布,并且今天出现了新的提交和合并请求天。

解析器


在文档页面上,我找到了confirmation_option装饰器,该装饰器在执行命令之前要求用户确认。 为了演示它,添加了擦拭命令,该命令清除了整个任务列表。

源代码
 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) # click   ,     @click.option("--db", help="Path to the database file") @click.pass_context def cli(ctx, verbose, db): """Todo notes - click version.""" level = levels[min(verbose, 2)] logging.basicConfig(level=level) logging.getLogger("todolib").setLevel(level) ctx.obj = TodoApp.fromenv(db) atexit.register(ctx.obj.save) @cli.command() @click.argument("task") @pass_app def add(app, task): """ Add new task. """ task = app.add_task(task) click.echo(f"{task} created with number {task.number}.") @cli.command() @click.option("--show-done", is_flag=True, help="Include done tasks") @pass_app def show(app, show_done): """ Show current tasks. """ app.print_tasks(show_done) @cli.command() @click.argument("number", type=int) @pass_app def done(app, number): """ Mark task as done. """ task = app.task_done(number) click.echo(f"{task} marked as done.") @cli.command() @click.argument("number", type=int) @pass_app def remove(app, number): """ Remove task from the list. """ task = app.remove_task(number) click.echo(f"{task} removed from the list.") @cli.command() @click.confirmation_option(prompt="Are you sure you want to remove database") @pass_app def wipe(app): for task in app.list_tasks(): task.remove() 


主要的


在这里,我们遇到了Click的主要优点-由于命令的逻辑根据它们的功能分开,因此几乎没有内容保留在main中。 这里还展示了库从环境变量接收参数和参数的能力。

 if __name__ == "__main__": cli(auto_envvar_prefix="TODO") 

测试中


对于Click,不需要拦截sys.stdout,因为有一个带有运行程序的click.testing模块可以处理这些事情。 CliRunner本身不仅拦截输出,还允许您检查退出代码,这也很酷。 所有这一切都允许在不使用pytest并绕过标准unittest模块的情况下测试点击实用程序。

 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 

总结


这只是Click可以做的一小部分。 从API的其余部分开始-值的验证,与终端的集成(颜色,较少的分页器,进度条等),结果回调,自动完成等。 您可以在此处查看他们的示例。

优点:在任何情况下都有很多工具,是原始的,但是同时方便描述团队,易于测试和活跃项目寿命。

缺点:“单击”的缺点是什么-这是一个难题。 也许他对以下库的功能一无所知?

着火


Fire不仅是Google提供的用于控制台界面的年轻库(于2017年出现),它还是一个用于以逐字引号生成绝对任何 Python 对象的控制台界面的库。
除其他外,据指出,fire有助于代码的开发和调试,有助于调整CLI中的现有代码,促进从bash到Python的过渡,并具有自己的REPL用于交互工作。 我们可以看到吗?

解析器和主


fire.Fire实际上能够接受任何对象:模块,类实例,具有命令名称和相应功能的字典等。

对我们而言重要的是Fire允许传输类对象。 因此,类构造函数接受所有命令共有的参数,并且其方法和属性是单独的命令。 我们将使用此:

源代码
 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) 


内联标志


Fire有自己的带有特殊语法的标志(必须在“-”之后传递它们),这使您可以在解析器和整个应用程序的内部进行查看:

通话范例
 $ ./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 #    , ,  Commands._app NAME todo_fire.py - SYNOPSIS todo_fire.py - GROUP | COMMAND GROUPS GROUP is one of the following: _app Todo Application definition. $ ./todo_fire.py show -- --interactive Fire is starting a Python REPL with the following objects: Modules: atexit, fire, logging, todolib Objects: Commands, args, component, main, result, self, todo_fire.py, trace Python 3.7.4 (default, Aug 15 2019, 13:09:37) [GCC 7.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> self <__main__.Commands object at 0x7fd0a6125bd0> >>> self._app.db {'tasks': [{'title': 'test', 'done': False}]} 


测试中


测试主要功能与测试argparse和docopt相似,因此我在这里看不到重点。

同时,值得注意的是,由于Fire具有自省性,因此同样有可能立即测试Commands类。

总结


火是一种与单击同样有趣的工具。 它不需要在解析器中列出很多选项,配置最少,有用于调试的选项,并且库本身的生存和开发远比单击(今年夏天提交60项)更为活跃。

缺点:可以大大少于点击和其他解析器; 不稳定的API(当前版本为0.2.1)。

水泥厂


实际上, Cement并不完全是CLI库,而是用于控制台应用程序的框架,但是据称它适用于具有各种集成的脚本和复杂应用程序。

解析器


Cement中的解析器看起来很不寻常,但是如果仔细看一下参数,就很容易猜到熟悉的argparse是在幕后。 但这也许是最好的-无需学习新参数。

源代码
 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.") 


应用程式和主要


除其他事项外,水泥仍然将异常包裹起来。 这在SIGINT / SIGTERM的零代码输出中得到了证明。

源代码
 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: # application label label = "todo_cement" # register handlers handlers = [Base] hooks = [("post_setup", lambda app: app.load_db()), ("pre_close", lambda app: app.save())] # call sys.exit() on close close_on_exit = True def main(): with TodoApp() as app: try: app.run() except CaughtSignal as e: if e.signum not in (signal.SIGINT, signal.SIGTERM): raise app.log.debug(f"\n{e}") app.exit_code = 0 


如果您读过main,则可以看到todolib.TodoApp的加载和保存也可以在覆盖的__enter __ / __ exit__中完成,但是这些阶段最终被分成了单独的方法,以演示Cement钩子。

测试中


为了进行测试,您可以使用相同的应用程序类:

 def test_cement(capsys): with todo_cement.TodoApp(argv=["add", "test"]) as app: app.run() out, _ = capsys.readouterr() assert out == EXPECTED #     jinja,       assert app.last_rendered is None 

总结


优点:这套API就像一套瑞士刀,可通过钩子和插件进行扩展,具有稳定的界面和活跃的开发能力。

缺点:在空的文档中; 小型的基于Cement的脚本似乎有些复杂。

克莱奥


Cleo远不像这里列出的其他框架那样受欢迎(在GitHub上总共有400颗星),但是当我研究Poetry格式化输出时,我设法了解了它。

因此,Cleo是已经提到的Poetry(用于管理依赖项,virtualenv和应用程序构建的工具)的作者的项目之一。 关于habr上的诗歌已经写了不止一次了,关于它的控制台部分-不。

解析器


像水泥一样,克莱奥(Cleo)也建立在对象原则的基础上,即 通过Command类及其文档字符串定义命令,通过option()方法访问参数,依此类推。 另外,用于输出文本的line()方法支持样式(即颜色)并根据开箱即用的详细标记数输出过滤。 Cleo还具有表输出。 还有进度条。 但是...一般而言,请参阅:

源代码
 from cleo import Command as BaseCommand # cleo    clikit,          from clikit.api.io import flags as verbosity class Command(BaseCommand): def __init__(self): super().__init__() self.todoapp = None def handle(self): with todolib.TodoApp.fromenv() as app: self.todoapp = app self.do_handle() def do_handle(self): raise NotImplementedError class AddCommand(Command): """ Add new task. add {task : Task to add} """ def do_handle(self): title = self.argument("task") task = self.todoapp.add_task(title) # will be printed only on "-vvv" self.line(f"Title: {title}", style="comment", verbosity=verbosity.DEBUG) self.line(f"Task <info>{task.title}</> created with number {task.number}.") class ShowCommand(Command): """ Show current tasks. show {--show-done : Include tasks that are done.} """ def do_handle(self): tasks = self.todoapp.list_tasks(self.option("show-done")) if not tasks: self.line("There is no TODOs.", style="info") self.render_table( ["Number", "Title", "Status"], [ [str(task.number), task.title, "" if task.done else "✘"] for task in tasks ], ) class DoneCommand(Command): """ Mark task as done. done {number : Task number} """ def do_handle(self): task = self.todoapp.task_done(int(self.argument("number"))) self.line(f"Task <info>{task.title}</> marked as done.") class RemoveCommand(Command): """ Removes task from the list. remove {number : Task number} """ def do_handle(self): task = self.todoapp.remove_task(int(self.argument("number"))) self.line(f"Task <info>{task.title}</> removed from the list.") 


主要的


需要做的只是创建一个cleo.Application对象,然后将命令传递给add_commands。 为了在测试期间不重复,所有这些都从main转移到了构造函数:

 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) 

测试中


为了在Cleo中测试命令,有CommandTester ,它框架的所有成年叔叔一样,拦截I / O并退出代码:

 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" 

总结


优点:带类型提示的对象结构,可简化开发(因为许多IDE和编辑器都对OOP代码和键入模块提供了很好的支持); 大量的功能不仅可以处理参数,还可以处理I / O。

加或减:其详细参数,仅与I / O Cleo / CliKit兼容。 尽管可以为日志记录模块编写自定义处理程序,但是随着cleo的开发,可能很难维护它。

缺点:很明显-个人观点-一个年轻的API:该框架除“诗歌”外缺少另一个“大型”用户,Cleo的开发与开发并行并满足一个人的需求; 有时文档过时了(例如,日志记录级别现在不在clikit模块中,而是在clikit.api.io.flags中),并且通常情况下效果不佳,无法反映整个API。

与Cement相比,Cleo更加专注于CLI,并且他是唯一考虑过格式化默认输出中的异常(隐藏默认堆栈跟踪)的人。 但是,他(也是个人观点)在其青年时期和API的稳定性方面输给了Cement。

总结


至此,每个人都已经有了自己的看法,这是更好的,但结论应该是:我最喜欢Click,因为其中包含很多东西,并且使用它开发和测试应用程序非常容易。 如果您尝试编写最少的代码-请从Fire开始。 您的脚本需要访问Memcached,使用jinja进行格式设置和可扩展性-使用Cement,您将不会后悔。 您有一个宠物项目,或者想尝试其他东西-看一下cleo。

Source: https://habr.com/ru/post/zh-CN466999/


All Articles