具有多个子命令的 Python 命令行工具的最佳架构
Best architecture for a Python command-line tool with multiple subcommands
我正在为项目开发命令行工具集。最终工具将支持许多子命令,例如
foo command1 [--option1 [value]?]*
所以可以有像
这样的子命令
foo create --option1 value --
foo make file1 --option2 --option3
该工具使用 argparse 库来处理命令行参数和帮助功能等
一些额外的要求和限制:
某些选项和功能对于所有子命令都是相同的(例如解析 YAML 配置文件等)
一些子命令编码起来既快速又简单,因为它们例如只需调用外部 bash 脚本。
一些子命令会很复杂,因此代码很长。
基本工具和单个子命令的帮助应该可用:
foo 帮助
可用命令有:make、create、add、xyz
foo 帮助制作
make 子命令的详细信息
子命令中的错误代码应该是统一的(就像“找不到文件”的相同错误代码)
出于调试目的并在最小可行版本的自包含功能方面取得进展,我想将一些子命令开发为自包含脚本和模块,例如
make.py
可以导入到主 foo.py
脚本中,稍后作为两者调用
make.py --option1 value etc.
和
foo.py make --option1 value
现在,我的问题是:以最小冗余度模块化如此复杂的 CLI 工具的最佳方法是什么(例如,参数定义和解析应该只在一个组件中编码)?
方案一:把所有东西都放在一个大脚本里,但是这样会很难管理。
选项 2: 在单个模块/文件中开发子命令的功能(如 make.py
、add.py
);但这样必须保持可调用(通过 if __name__ == '__main__' ...
)。
然后可以将子命令模块中的函数导入主脚本,并将子命令中的解析器和参数添加为子解析器。
选项 3: 主脚本可以简单地重新格式化对子命令的调用以进行子处理,就像这样
subprocess.run('./make.py {arguments}', shell=True, check=True, text=True)
考虑使用 Command Pattern along with the Factory Method Pattern。
简而言之,创建一个名为 Command 的抽象 class 并使每个命令都是自己的 class 继承自 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()
我比较习惯回答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
if name == 'main':
解析器 = make_parser(子列表)
参数 = parser.parse_args()
# print(args) # 调试显示
args.func(args) # 再次是子解析器 func
在submod1.py
导入 argparse
def make_parser():
parser = argparse.ArgumentParser(add_help=False) # 检查文档?
parser.add_argument(...) # 可以在这里添加一个普通的 parents
return 解析器
parser.make_parser()
def func(args):
# module specific 'main'
我敢肯定这在很多方面都不完整,因为我是在没有测试的情况下临时写的。这是记录的基本子解析器定义,但使用 parents
导入子模块中定义的子解析器。 parents
也可用于定义子解析器的公共参数;但效用函数也同样有效。我认为 parents
在使用您无法访问的解析器时最有用; IE。进口的。
parents
本质上是将 Actions 从一个解析器复制到新的解析器——按引用复制(不是按值或作为副本)。它不是一个高度开发的工具,并且已经有许多人 运行 遇到问题的 SO。所以不要试图过度扩展它。
感谢您的所有建议!
我认为最优雅的方法是使用 Typer 并遵循以下秘诀:
我正在为项目开发命令行工具集。最终工具将支持许多子命令,例如
foo command1 [--option1 [value]?]*
所以可以有像
这样的子命令foo create --option1 value --
foo make file1 --option2 --option3
该工具使用 argparse 库来处理命令行参数和帮助功能等
一些额外的要求和限制:
某些选项和功能对于所有子命令都是相同的(例如解析 YAML 配置文件等)
一些子命令编码起来既快速又简单,因为它们例如只需调用外部 bash 脚本。
一些子命令会很复杂,因此代码很长。
基本工具和单个子命令的帮助应该可用:
foo 帮助 可用命令有:make、create、add、xyz
foo 帮助制作 make 子命令的详细信息
子命令中的错误代码应该是统一的(就像“找不到文件”的相同错误代码)
出于调试目的并在最小可行版本的自包含功能方面取得进展,我想将一些子命令开发为自包含脚本和模块,例如
make.py
可以导入到主 foo.py
脚本中,稍后作为两者调用
make.py --option1 value etc.
和
foo.py make --option1 value
现在,我的问题是:以最小冗余度模块化如此复杂的 CLI 工具的最佳方法是什么(例如,参数定义和解析应该只在一个组件中编码)?
方案一:把所有东西都放在一个大脚本里,但是这样会很难管理。
选项 2: 在单个模块/文件中开发子命令的功能(如 make.py
、add.py
);但这样必须保持可调用(通过 if __name__ == '__main__' ...
)。
然后可以将子命令模块中的函数导入主脚本,并将子命令中的解析器和参数添加为子解析器。
选项 3: 主脚本可以简单地重新格式化对子命令的调用以进行子处理,就像这样
subprocess.run('./make.py {arguments}', shell=True, check=True, text=True)
考虑使用 Command Pattern along with the Factory Method Pattern。
简而言之,创建一个名为 Command 的抽象 class 并使每个命令都是自己的 class 继承自 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()
我比较习惯回答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
if name == 'main': 解析器 = make_parser(子列表) 参数 = parser.parse_args() # print(args) # 调试显示 args.func(args) # 再次是子解析器 func
在submod1.py
导入 argparse def make_parser(): parser = argparse.ArgumentParser(add_help=False) # 检查文档? parser.add_argument(...) # 可以在这里添加一个普通的 parents return 解析器
parser.make_parser()
def func(args):
# module specific 'main'
我敢肯定这在很多方面都不完整,因为我是在没有测试的情况下临时写的。这是记录的基本子解析器定义,但使用 parents
导入子模块中定义的子解析器。 parents
也可用于定义子解析器的公共参数;但效用函数也同样有效。我认为 parents
在使用您无法访问的解析器时最有用; IE。进口的。
parents
本质上是将 Actions 从一个解析器复制到新的解析器——按引用复制(不是按值或作为副本)。它不是一个高度开发的工具,并且已经有许多人 运行 遇到问题的 SO。所以不要试图过度扩展它。
感谢您的所有建议!
我认为最优雅的方法是使用 Typer 并遵循以下秘诀: