ArgumentParser 的自定义冲突处理
Custom conflict handling for ArgumentParser
我需要的
我需要一个 ArgumentParser
,具有冲突处理方案,可以解决一些已注册的重复参数集,但会引发所有其他参数。
我试过的
我最初的做法(另请参阅底部的代码示例)是 subclass ArgumentParser
,添加一个 _handle_conflict_custom
方法,然后实例化 subclass 与 ArgumentParser(conflict_handler='custom')
,认为 _get_handler
方法会拾取它。
问题
这会报错,因为ArgumentParser
继承自_ActionsContainer
,它提供了_get_handler
和_handle_conflict_{strategy}
方法,然后在内部实例化了一个_ArgumentGroup
(也继承自 _ActionsContainer
),它又不知道 ArgumentParser
上新定义的方法,因此无法获得自定义处理程序。
出于同样的原因,覆盖 _get_handler
方法是不可行的。
我已经创建了一个 (rudimentary) class 图表来说明关系,因此希望问题在 subclassing ArgumentParser
实现我想要的。
动机
我(认为,我)需要这个,因为我有两个脚本,它们处理工作流的不同部分,我希望能够将它们分别用作脚本,但也有一个脚本,导入这两个脚本的方法,一气呵成。
这个脚本应该支持两个单独脚本的所有选项,但我不想复制(大量的)参数定义,这样我就不得不在多个地方进行更改。
这很容易通过导入(部分)脚本的 ArgumentParsers
并将它们用作父脚本来解决,就像 combined_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')
其他问题
我知道这些类似的问题:
- python argparse subcommand with dependency and conflict
- argparse conflict when used with two connected python3 scripts
- ... and others
但尽管其中一些提供了对 argparse 用法和冲突的有趣见解,但它们似乎解决了与我无关的问题。
出于各种原因——尤其是测试的需要——我采用了
总是以数据的形式定义 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:])
基于 FMcs 方法,我创建了一些更精细的东西,我知道这不是代码审查,但仍然欢迎反馈。另外,也许它可以帮助别人看到这个更加充实。
import argparse
from collections import Counter, OrderedDict
from typing import List, Dict, Any
from copy import deepcopy
class OptionConf:
def __init__(self):
self._conf = OrderedDict() # type: Dict[str, List[Dict[str, Any]]]
self._allowed_dupes = list() # type: List[str]
def add_conf(self, command, *conf_args, **conf_kwargs):
if command not in self._conf:
self._conf[command] = []
conf_kwargs['*'] = conf_args
self._conf[command].append(conf_kwargs)
def add_argument(self, *conf_args, **conf_kwargs):
self.add_conf('add_argument', *conf_args, **conf_kwargs)
def register_allowed_duplicate(self, flag):
self._allowed_dupes.append(flag)
def generate_parser(self, **kwargs):
argument_parser = argparse.ArgumentParser(**kwargs)
for command, conf_kwargs_list in self._conf.items():
command_func = getattr(argument_parser, command)
for conf_kwargs in conf_kwargs_list:
list_args = conf_kwargs.pop('*', [])
command_func(*list_args, **conf_kwargs)
conf_kwargs['*'] = list_args
return argument_parser
def _get_add_argument_conf_args(self):
for command, kwargs_list in self._conf.items():
if command != 'add_argument':
continue
return kwargs_list
return []
def resolve_registered(self, other):
if self.__class__ == other.__class__:
conf_args_list = self._get_add_argument_conf_args() # type: List[Dict[str, Any]]
other_conf_args_list = other._get_add_argument_conf_args() # type: List[Dict[str, Any]]
# find all argument names of both parsers
all_names = []
for conf_args in conf_args_list:
all_names += conf_args.get('*', [])
all_other_names = []
for other_conf_args in other_conf_args_list:
all_other_names += other_conf_args.get('*', [])
# check for dupes and throw if appropriate
found_allowed_dupes = []
tally = Counter(all_names + all_other_names)
for name, count in tally.items():
if count > 1 and name not in self._allowed_dupes:
raise Exception(f'Duplicate argument configurations: {name}')
elif count > 1:
found_allowed_dupes.append(name)
# merge them in a new OptionConf, preferring the args of self (AS OPPOSED TO ORIGINAL RESOLVE)
new_opt_conf = OptionConf()
for command, kwargs_list in self._conf.items():
for kwargs in kwargs_list:
list_args = kwargs.get('*', [])
new_opt_conf.add_conf(command, *list_args, **kwargs)
for command, kwargs_list in other._conf.items():
for kwargs in kwargs_list:
# if it's another argument, we remove dupe names
if command == 'add_argument':
all_names = kwargs.pop('*', [])
names = [name for name in all_names if name not in found_allowed_dupes]
# and only add if there are names left
if names:
new_opt_conf.add_argument(*deepcopy(names), **deepcopy(kwargs))
# put names back
kwargs['*'] = all_names
else:
# if not, we just add it
list_args = kwargs.pop('*', [])
new_opt_conf.add_conf(command, *deepcopy(list_args), **deepcopy(kwargs))
# put list args back
kwargs['*'] = list_args
return new_opt_conf
raise NotImplementedError()
if __name__ == '__main__':
opts_conf = OptionConf()
opts_conf.add_argument('pos_arg')
opts_conf.add_argument('-n', '--number', metavar='N', type=int)
opts_conf.add_argument('-i', '--index')
opts_conf.add_argument('-v', '--verbose', action='store_true')
opts_conf2 = OptionConf()
opts_conf2.add_argument('-n', '--number', metavar='N', type=int)
opts_conf2.add_argument('-v', action='store_true')
opts_conf.register_allowed_duplicate('-n')
opts_conf.register_allowed_duplicate('--number')
try:
resolved_opts = opts_conf.resolve_registered(opts_conf2)
except Exception as e:
print(e) # raises on -v
opts_conf.register_allowed_duplicate('-v')
resolved_opts = opts_conf.resolve_registered(opts_conf2)
ap = resolved_opts.generate_parser(description='does it work?')
ap.parse_args(['-h'])
虽然我同意 FMc 的方法在长期可行性方面可能是更好的方法,但我找到了一种将自定义处理程序覆盖到 ArgumentParser 中的方法。
关键是覆盖实际定义处理程序函数的 _ActionsContainer class。然后覆盖 ArgumentParser 和 _ArgumentGroup 继承自的基础 classes。
在下面的例子中,我只是添加了一个忽略任何冲突的处理程序,但您可以添加任何您想要的自定义逻辑。
import argparse
class IgnorantActionsContainer(argparse._ActionsContainer):
def _handle_conflict_ignore(self, action, conflicting_actions):
pass
argparse.ArgumentParser.__bases__ = (argparse._AttributeHolder, IgnorantActionsContainer)
argparse._ArgumentGroup.__bases__ = (IgnorantActionsContainer,)
parser = argparse.ArgumentParser(conflict_handler="ignore")
parser.add_argument("-a", type=int, default=1)
parser.add_argument("-a", type=int, default=2)
parser.add_argument("-a", type=int, default=3)
parser.add_argument("-a", type=int, default=4)
print(parser.parse_args())
运行 python custom_conflict_handler.py -h
打印:
usage: custom_conflict_handler.py [-h] [-a A] [-a A] [-a A] [-a A]
optional arguments:
-h, --help show this help message and exit
-a A
-a A
-a A
-a A
运行 python custom_conflict_handler.py
打印:
Namespace(a=1)
运行 python custom_conflict_handler.py -a 5
打印:
Namespace(a=5)
我需要的
我需要一个 ArgumentParser
,具有冲突处理方案,可以解决一些已注册的重复参数集,但会引发所有其他参数。
我试过的
我最初的做法(另请参阅底部的代码示例)是 subclass ArgumentParser
,添加一个 _handle_conflict_custom
方法,然后实例化 subclass 与 ArgumentParser(conflict_handler='custom')
,认为 _get_handler
方法会拾取它。
问题
这会报错,因为ArgumentParser
继承自_ActionsContainer
,它提供了_get_handler
和_handle_conflict_{strategy}
方法,然后在内部实例化了一个_ArgumentGroup
(也继承自 _ActionsContainer
),它又不知道 ArgumentParser
上新定义的方法,因此无法获得自定义处理程序。
出于同样的原因,覆盖 _get_handler
方法是不可行的。
我已经创建了一个 (rudimentary) class 图表来说明关系,因此希望问题在 subclassing ArgumentParser
实现我想要的。
动机
我(认为,我)需要这个,因为我有两个脚本,它们处理工作流的不同部分,我希望能够将它们分别用作脚本,但也有一个脚本,导入这两个脚本的方法,一气呵成。
这个脚本应该支持两个单独脚本的所有选项,但我不想复制(大量的)参数定义,这样我就不得不在多个地方进行更改。
这很容易通过导入(部分)脚本的 ArgumentParsers
并将它们用作父脚本来解决,就像 combined_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')
其他问题
我知道这些类似的问题:
- python argparse subcommand with dependency and conflict
- argparse conflict when used with two connected python3 scripts
- ... and others
但尽管其中一些提供了对 argparse 用法和冲突的有趣见解,但它们似乎解决了与我无关的问题。
出于各种原因——尤其是测试的需要——我采用了 总是以数据的形式定义 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:])
基于 FMcs 方法,我创建了一些更精细的东西,我知道这不是代码审查,但仍然欢迎反馈。另外,也许它可以帮助别人看到这个更加充实。
import argparse
from collections import Counter, OrderedDict
from typing import List, Dict, Any
from copy import deepcopy
class OptionConf:
def __init__(self):
self._conf = OrderedDict() # type: Dict[str, List[Dict[str, Any]]]
self._allowed_dupes = list() # type: List[str]
def add_conf(self, command, *conf_args, **conf_kwargs):
if command not in self._conf:
self._conf[command] = []
conf_kwargs['*'] = conf_args
self._conf[command].append(conf_kwargs)
def add_argument(self, *conf_args, **conf_kwargs):
self.add_conf('add_argument', *conf_args, **conf_kwargs)
def register_allowed_duplicate(self, flag):
self._allowed_dupes.append(flag)
def generate_parser(self, **kwargs):
argument_parser = argparse.ArgumentParser(**kwargs)
for command, conf_kwargs_list in self._conf.items():
command_func = getattr(argument_parser, command)
for conf_kwargs in conf_kwargs_list:
list_args = conf_kwargs.pop('*', [])
command_func(*list_args, **conf_kwargs)
conf_kwargs['*'] = list_args
return argument_parser
def _get_add_argument_conf_args(self):
for command, kwargs_list in self._conf.items():
if command != 'add_argument':
continue
return kwargs_list
return []
def resolve_registered(self, other):
if self.__class__ == other.__class__:
conf_args_list = self._get_add_argument_conf_args() # type: List[Dict[str, Any]]
other_conf_args_list = other._get_add_argument_conf_args() # type: List[Dict[str, Any]]
# find all argument names of both parsers
all_names = []
for conf_args in conf_args_list:
all_names += conf_args.get('*', [])
all_other_names = []
for other_conf_args in other_conf_args_list:
all_other_names += other_conf_args.get('*', [])
# check for dupes and throw if appropriate
found_allowed_dupes = []
tally = Counter(all_names + all_other_names)
for name, count in tally.items():
if count > 1 and name not in self._allowed_dupes:
raise Exception(f'Duplicate argument configurations: {name}')
elif count > 1:
found_allowed_dupes.append(name)
# merge them in a new OptionConf, preferring the args of self (AS OPPOSED TO ORIGINAL RESOLVE)
new_opt_conf = OptionConf()
for command, kwargs_list in self._conf.items():
for kwargs in kwargs_list:
list_args = kwargs.get('*', [])
new_opt_conf.add_conf(command, *list_args, **kwargs)
for command, kwargs_list in other._conf.items():
for kwargs in kwargs_list:
# if it's another argument, we remove dupe names
if command == 'add_argument':
all_names = kwargs.pop('*', [])
names = [name for name in all_names if name not in found_allowed_dupes]
# and only add if there are names left
if names:
new_opt_conf.add_argument(*deepcopy(names), **deepcopy(kwargs))
# put names back
kwargs['*'] = all_names
else:
# if not, we just add it
list_args = kwargs.pop('*', [])
new_opt_conf.add_conf(command, *deepcopy(list_args), **deepcopy(kwargs))
# put list args back
kwargs['*'] = list_args
return new_opt_conf
raise NotImplementedError()
if __name__ == '__main__':
opts_conf = OptionConf()
opts_conf.add_argument('pos_arg')
opts_conf.add_argument('-n', '--number', metavar='N', type=int)
opts_conf.add_argument('-i', '--index')
opts_conf.add_argument('-v', '--verbose', action='store_true')
opts_conf2 = OptionConf()
opts_conf2.add_argument('-n', '--number', metavar='N', type=int)
opts_conf2.add_argument('-v', action='store_true')
opts_conf.register_allowed_duplicate('-n')
opts_conf.register_allowed_duplicate('--number')
try:
resolved_opts = opts_conf.resolve_registered(opts_conf2)
except Exception as e:
print(e) # raises on -v
opts_conf.register_allowed_duplicate('-v')
resolved_opts = opts_conf.resolve_registered(opts_conf2)
ap = resolved_opts.generate_parser(description='does it work?')
ap.parse_args(['-h'])
虽然我同意 FMc 的方法在长期可行性方面可能是更好的方法,但我找到了一种将自定义处理程序覆盖到 ArgumentParser 中的方法。
关键是覆盖实际定义处理程序函数的 _ActionsContainer class。然后覆盖 ArgumentParser 和 _ArgumentGroup 继承自的基础 classes。
在下面的例子中,我只是添加了一个忽略任何冲突的处理程序,但您可以添加任何您想要的自定义逻辑。
import argparse
class IgnorantActionsContainer(argparse._ActionsContainer):
def _handle_conflict_ignore(self, action, conflicting_actions):
pass
argparse.ArgumentParser.__bases__ = (argparse._AttributeHolder, IgnorantActionsContainer)
argparse._ArgumentGroup.__bases__ = (IgnorantActionsContainer,)
parser = argparse.ArgumentParser(conflict_handler="ignore")
parser.add_argument("-a", type=int, default=1)
parser.add_argument("-a", type=int, default=2)
parser.add_argument("-a", type=int, default=3)
parser.add_argument("-a", type=int, default=4)
print(parser.parse_args())
运行 python custom_conflict_handler.py -h
打印:
usage: custom_conflict_handler.py [-h] [-a A] [-a A] [-a A] [-a A]
optional arguments:
-h, --help show this help message and exit
-a A
-a A
-a A
-a A
运行 python custom_conflict_handler.py
打印:
Namespace(a=1)
运行 python custom_conflict_handler.py -a 5
打印:
Namespace(a=5)