
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也不难学习:有一个解析器,有参数,这些参数具有
type ,
action ,
dest ,
default和
help 。 并具有子
解析器 -能够将部分参数和逻辑分离为单独的命令。
解析器
乍一看-没什么异常,解析器就像解析器。 但是-我认为-与其他库相比,可读性不是最好的,因为 不同命令的参数在一个地方描述。
源代码 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的主要优点-由于命令的逻辑根据它们的功能分开,因此几乎没有内容保留在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
测试中
测试主要功能与测试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:
如果您读过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
总结
优点:这套API就像一套瑞士刀,可通过钩子和插件进行扩展,具有稳定的界面和活跃的开发能力。
缺点:在空的文档中; 小型的基于Cement的脚本似乎有些复杂。
克莱奥
Cleo远不像这里列出的其他框架那样受欢迎(在GitHub上总共有400颗星),但是当我研究Poetry格式化输出时,我设法了解了它。
因此,Cleo是已经提到的Poetry(用于管理依赖项,virtualenv和应用程序构建的工具)的作者的项目之一。 关于habr上的诗歌已经写了不止一次了,关于它的控制台部分-不。
解析器
像水泥一样,克莱奥(Cleo)也建立在对象原则的基础上,即 通过Command类及其文档字符串定义命令,通过option()方法访问参数,依此类推。 另外,用于输出文本的line()方法支持样式(即颜色)并根据开箱即用的详细标记数输出过滤。 Cleo还具有表输出。 还有进度条。 但是...一般而言,请参阅:
源代码 from cleo import Command as BaseCommand
主要的
需要做的只是创建一个
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。