具有多个子命令的 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.pyadd.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】:

我更习惯于回答有关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

如果 name == 'ma​​in': 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 命令行工具的最佳架构的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 python argparse 解析多个嵌套的子命令?

开发命令行工具的 12 个最佳实践

具有依赖和冲突的python argparse子命令

具有多个命令行选项的 pytest

python开发简单的命令行工具

在Windows中递归删除具有指定名称的文件夹的命令行工具?