具有多个子命令的 Python 命令行工具的最佳架构
Posted
技术标签:
【中文标题】具有多个子命令的 Python 命令行工具的最佳架构【英文标题】:Best architecture for a Python command-line tool with multiple subcommands 【发布时间】:2021-11-23 10:03:39 【问题描述】:我正在为一个项目开发一个命令行工具集。最终的工具应该支持很多子命令,像这样
foo command1 [--option1 [value]?]*
所以可以有类似的子命令
foo create --option1 value --
foo make file1 --option2 --option3
该工具使用 argparse 库来处理命令行参数和帮助功能等。
一些额外的要求和限制:
所有子命令的某些选项和功能都是相同的(例如解析 YAML 配置文件等)
一些子命令的编码既快速又简单,因为它们例如只需调用外部 bash 脚本即可。
一些子命令会很复杂,因此代码很长。
应该提供基本工具的帮助以及单个子命令的帮助:
帮助 可用的命令有:make、create、add、xyz
foo 帮助制作 make 子命令的详细信息
子命令的错误代码应该是统一的(就像“找不到文件”的错误代码一样)
出于调试目的和在最小可行版本的自包含功能方面取得进展,我想开发一些子命令作为自包含脚本和模块,例如
make.py
可以导入到主 foo.py
脚本中,然后作为两者调用
make.py --option1 value etc.
和
foo.py make --option1 value
现在,我的问题是:用最少的冗余来模块化这样一个复杂的 CLI 工具的最佳方法是什么(例如,参数定义和解析应该只编码在一个组件中)?
选项 1:将所有内容放在一个大脚本中,但这将变得难以管理。
选项 2: 在单个模块/文件中开发子命令的功能(如 make.py
、add.py
);但必须保持可调用(通过if __name__ == '__main__' ...
)。
然后可以将子命令模块中的函数导入到主脚本中,并将子命令中的解析器和参数添加为子解析器。
选项3:主脚本可以简单地重新格式化对子进程的子命令的调用,就像这样
subprocess.run('./make.py arguments', shell=True, check=True, text=True)
【问题讨论】:
python 库 'click' 提供了这个功能 Typer 也是如此,如果你习惯了 FastAPI 我认为您的问题比重复链接要大得多。parents
是一种懒人在多个子解析器中定义相同参数的方式。但懒惰的程序员也知道他们可以编写辅助函数来执行重复性任务。但我认为 SO 不是解决程序结构问题的好论坛。范围太大,太受制于意见。一旦您编写了基本的子解析器代码,您就已经用尽了argparse
提供的工具。在模块中运行子命令和分区不是argparse
问题。
没错。最佳实践问题,如果它们是关于 Stack Exchange 的主题,属于 Software Engineering,而不是这里。 Stack Overflow 仅适用于有关在编程实践中遇到的狭窄、具体问题的问题,这些问题可以得到规范的答案。
Typer(以及,为了 API 目的,FastAPI)看起来很有前途!谢谢!
【参考方案1】:
我更习惯于回答有关numpy
和argparse
的详细信息的问题,但这是我对大包的设想。
在main.py
:
import submod1
# ....
sublist = [submod1, ...]
def make_parser(sublist):
parser = argparse.ArgumentParser()
# parser.add_argument('-f','--foo') # main specific
# I'd avoid positionals
sp = parser.add_subparsers(dest='cmd', etc)
splist=[]
for md in sublist:
sp1 = sp.add_parser(help='', parents=[md.parser])
sp1.set_default(func=md.func) # subparser func as shown in docs
splist.append(sp1)
return parser
如果 name == 'main': parser = make_parser(子列表) args = parser.parse_args() # print(args) # 调试显示 args.func(args) # 再次是子解析器函数
在submod1.py
导入参数解析 def make_parser(): parser = argparse.ArgumentParser(add_help=False) # 检查文档? parser.add_argument(...) # 可以在这里添加一个共同的父母 返回解析器
parser.make_parser()
def func(args):
# module specific 'main'
我确信这在很多方面都是不完整的,因为我是在未经测试的情况下即时编写的。这是一个基本的子解析器定义,但使用parents
导入子模块中定义的子解析器。 parents
也可用于定义子解析器的公共参数;但是实用功能也可以。我认为parents
在使用您无法访问的解析器时最有用; IE。一个进口的。
parents
本质上是从一个解析器复制动作到新的解析器——通过引用复制(不是通过值或作为副本)。它不是一个高度发达的工具,并且有许多 SO 让人们遇到问题。所以不要试图过度扩展它。
【讨论】:
【参考方案2】:考虑使用Command Pattern 和Factory Method Pattern。
简而言之,创建一个名为 Command 的抽象类,并使每个命令都成为继承自 Command 的自己的类。
例子:
class Command():
def execute(self):
raise NotImplementedError()
class Command1(Command):
def __init__(self, *args):
pass
def execute(self):
pass
class Command2(Command):
def __init__(self, *args):
pass
def execute(self):
pass
这将处理命令的执行。对于建筑,制作一个命令工厂。
class CommandFactory():
@staticmethod
def create(command, *args):
if command == 'command1':
return Command1(args)
elif command == 'command2':
return Command2(args)
那么你就可以用一行来执行命令了:
CommandFactory.create(command, args).execute()
【讨论】:
嗯 - 感谢您的努力!但是,问题不在于添加子命令——argparse 很好地支持了这一点。主要挑战是模块化方法,它消除了有关 CLI 参数和文档/帮助的冗余,同时允许将子命令作为单独的 Python 脚本运行。【参考方案3】:感谢您的所有建议!
我认为最优雅的方法是使用 Typer 并遵循以下配方:
https://typer.tiangolo.com/tutorial/subcommands/add-typer/
【讨论】:
以上是关于具有多个子命令的 Python 命令行工具的最佳架构的主要内容,如果未能解决你的问题,请参考以下文章