具有多个子命令的 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 库来处理命令行参数和帮助功能等

一些额外的要求和限制:

出于调试目的并在最小可行版本的自包含功能方面取得进展,我想将一些子命令开发为自包含脚本和模块,例如

make.py

可以导入到主 foo.py 脚本中,稍后作为两者调用

make.py --option1 value etc.

foo.py make --option1 value

现在,我的问题是:以最小冗余度模块化如此复杂的 CLI 工具的最佳方法是什么(例如,参数定义和解析应该只在一个组件中编码)?

方案一:把所有东西都放在一个大脚本里,但是这样会很难管理。

选项 2: 在单个模块/文件中开发子命令的功能(如 make.pyadd.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()

我比较习惯回答numpyargparse的细节问题,但是下面是我对大包的设想。

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 并遵循以下秘诀:

https://typer.tiangolo.com/tutorial/subcommands/add-typer/