Custom conflict handling for ArgumentParser

Fyn*_*ynn 7 python subclass argparse

What I need

I need an ArgumentParser, with a conflict handling scheme, that resolves some registered set of duplicate arguments, but raises on all other arguments.

What I tried

My initial approach (see also the code example at the bottom) was to subclass ArgumentParser, add a _handle_conflict_custom method, and then instantiate the subclass with ArgumentParser(conflict_handler='custom'), thinking that the _get_handler method would pick it up.

The Problem

This raises an error, because the ArgumentParser inherits from _ActionsContainer, which provides the _get_handler and the _handle_conflict_{strategy} methods, and then internally instantiates an _ArgumentGroup (that also inherits from _ActionsContainer), which in turn doesn't know about the newly defined method on ArgumentParser and thus fails to get the custom handler.

Overriding the _get_handler method is not feasible for the same reasons.

I have created a (rudimentary) class diagram illustrating the relationships, and therefore hopefully the problem in subclassing ArgumentParser to achieve what I want.

类图.png

Motivation

我(认为我)需要这个,因为我有两个脚本,它们处理工作流程的不同部分,并且我希望能够将它们单独用作脚本,但也有一个脚本,它导入两个脚本的方法这些脚本,并且一次性完成所有事情。

该脚本应该支持两个单独脚本的所有选项,但我不想重复(大量)参数定义,因此我必须在多个位置进行更改。通过导入(部分)脚本并将它们用作父脚本
可以轻松解决此问题,如下所示。ArgumentParserscombined_parser = ArgumentParser(parents=[arg_parser1, arg_parser2])

在脚本中,我有重复的选项,例如工作目录,所以我需要解决这些冲突。
这也可以通过 来完成conflict_handler='resolve'

但是因为有很多可能的参数(这不取决于我们的团队,因为我们必须保持兼容性),我还希望脚本在定义导致冲突但尚未明确定义的内容时引发错误允许这样做,而不是悄悄地覆盖另一个标志,这可能会导致不需要的行为。

欢迎实现这些目标的其他建议(将两个脚本分开,允许使用一个脚本来包装这两个脚本,避免代码重复和引发意外的重复)。

示例代码

from argparse import ArgumentParser


class CustomParser(ArgumentParser):
    def _handle_conflict_custom(self, action, conflicting_actions):
        registered = ['-h', '--help', '-f']
        conflicts = conflicting_actions[:]

        use_error = False
        while conflicts:
            option_string, action = conflicts.pop()
            if option_string in registered:
                continue
            else:
                use_error = True
                break

        if use_error:
            self._handle_conflict_error(action, conflicting_actions)
        else:
            self._handle_conflict_resolve(action, conflicting_actions)


if __name__ == '__main__':
    ap1 = ArgumentParser()
    ap2 = ArgumentParser()

    ap1.add_argument('-f')  # registered, so should be resolved
    ap2.add_argument('-f')

    ap1.add_argument('-g')  # not registered, so should raise
    ap2.add_argument('-g')

    # this raises before ever resolving anything, for the stated reasons
    ap3 = CustomParser(parents=[ap1, ap2], conflict_handler='custom')


Run Code Online (Sandbox Code Playgroud)

其他问题

我知道这些类似的问题:

但是,尽管其中一些提供了有关 argparse 用法和冲突的有趣见解,但它们似乎解决了与我的无关的问题。

FMc*_*FMc 2

出于各种原因(尤其是测试的需要),我养成了始终以数据结构(通常是字典序列)的形式定义 argparse 配置的习惯。ArgumentParser 的实际创建是在一个可重用函数中完成的,该函数只是根据字典构建解析器。这种方法有很多好处,特别是对于更复杂的项目。

如果您的每个脚本都要转向该模型,我认为您可能能够检测到该函数中的任何配置冲突并相应地引发,从而避免需要从 ArgumentParser 继承并混乱地理解其内部结构。

我不确定我是否很好地理解您的冲突处理需求,因此下面的演示只是寻找重复的选项,并在看到一个选项时引发,但我认为您应该能够理解该方法并评估它是否适合您案件。基本思想是在普通数据结构领域而不是在 argparse 的拜占庭世界中解决问题

import sys
import argparse
from collections import Counter

OPTS_CONFIG1 = (
    {
        'names': 'path',
        'metavar': 'PATH',
    },
    {
        'names': '--nums',
        'nargs': '+',
        'type': int,
    },
    {
        'names': '--dryrun',
        'action': 'store_true',
    },
)

OPTS_CONFIG2 = (
    {
        'names': '--foo',
        'metavar': 'FOO',
    },
    {
        'names': '--bar',
        'metavar': 'BAR',
    },
    {
        'names': '--dryrun',
        'action': 'store_true',
    },
)

def main(args):
    ap = define_parser(OPTS_CONFIG1, OPTS_CONFIG2)
    opts = ap.parse_args(args)
    print(opts)

def define_parser(*configs):
    # Validation: adjust as needed.
    tally = Counter(
        nm
        for config in configs
        for d in config
        for nm in d['names'].split()
    )
    for k, n in tally.items():
        if n > 1:
            raise Exception(f'Duplicate argument configurations: {k}')

    # Define and return parser.
    ap = argparse.ArgumentParser()
    for config in configs:
        for d in config:
            kws = dict(d)
            xs = kws.pop('names').split()
            ap.add_argument(*xs, **kws)
    return ap

if __name__ == '__main__':
    main(sys.argv[1:])
Run Code Online (Sandbox Code Playgroud)