Создание компонуемых / иерархических парсеров командной строки с использованием Python/argparse
(Упрощенная форма проблемы.) Я пишу API, включающий некоторые компоненты Python. Это могут быть функции, но для конкретности, скажем, они объекты. Я хочу иметь возможность анализировать параметры для различных компонентов из командной строки.
from argparse import ArgumentParser
class Foo(object):
def __init__(self, foo_options):
"""do stuff with options"""
"""..."""
class Bar(object):
def __init__(sef, bar_options):
"""..."""
def foo_parser():
"""(could also be a Foo method)"""
p = ArgumentParser()
p.add_argument('--option1')
#...
return p
def bar_parser(): "..."
Но теперь я хочу иметь возможность создавать более крупные компоненты:
def larger_component(options):
f1 = Foo(options.foo1)
f2 = Foo(options.foo2)
b = Bar(options.bar)
# ... do stuff with these pieces
Хорошо. Но как написать соответствующий парсер? Мы могли бы пожелать что-то вроде этого:
def larger_parser(): # probably need to take some prefix/ns arguments
# general options to be overridden by p1, p2
# (this could be done automagically or by hand in `larger_component`):
p = foo_parser(prefix=None, namespace='foo')
p1 = foo_parser(prefix='first-foo-', namespace='foo1')
p2 = foo_parser(prefix='second-foo-', namespace='foo2')
b = bar_parser()
# (you wouldn't actually specify the prefix/namespace twice: )
return combine_parsers([(p1, namespace='foo1', prefix='first-foo-'),
(p2,...),p,b])
larger_component(larger_parser().parse_args())
# CLI should accept --foo1-option1, --foo2-option1, --option1 (*)
который выглядит как argparse
"s parents
Если вы забудете, что мы хотим использовать префиксы (чтобы можно было добавлять несколько парсеров одного типа) и, возможно, пространство имен (чтобы мы могли строить пространства имен с древовидной структурой, отражающие структуру компонентов).
Конечно, мы хотим, чтобы более крупные_компоненты и более крупные_парсеры можно было компоновать одинаково, а объект пространства имен, передаваемый определенному компоненту, должен всегда иметь одинаковую внутреннюю структуру формы / именования.
Беда в том, что argparse
API в основном заключается в мутировании ваших парсеров, но запрашивать их сложнее - если вы превратили тип данных в парсер напрямую, вы можете просто пройтись по этим объектам. Мне удалось взломать что-то, что несколько работает, если пользователь пишет кучу функций для добавления аргументов в парсеры вручную, но каждый add_argument
call должен затем взять префикс, и все это становится довольно непостижимым и, вероятно, несложным. (Вы можете абстрагироваться от этого за счет дублирования некоторых частей внутренних структур данных...). Я также пытался подкласс parser
а также group
объекты...
Вы можете представить, что это возможно при использовании более алгебраического API-интерфейса для анализа CLI, но я не думаю, что переписать argparse
здесь хорошее решение
Есть ли известный / простой способ сделать это?
2 ответа
Некоторые мысли, которые могут помочь вам создать больший парсер:
parser = argparse.ArgumentParser(...)
arg1 = parser.add_argument('--foo',...)
Сейчас arg1
это ссылка на Action
объект, созданный add_argument
, Я бы предложил сделать это в интерактивной оболочке и посмотреть на ее атрибуты. Или хотя бы распечатать его repr
, Вы также можете поэкспериментировать с изменением атрибутов. Большая часть того, что парсер "знает" об аргументах, содержится в этих actions
, В некотором смысле парсер - это объект, который "содержит" кучу "действий".
Посмотрите также на:
parser._actions
Это основной список действий парсера, который будет включать справку по умолчанию, а также те, которые вы добавляете.
parents
механизм копирования Action
ссылки от родителя к ребенку. Обратите внимание, что он не делает копии объектов Action. Он также воссоздает группы аргументов - но эти группы служат только для группировки строк справки. Они не имеют ничего общего с разбором.
args1, extras = parser.parse_known_args(argv, namespace)
очень полезно при работе с несколькими парсерами. С его помощью каждый синтаксический анализатор может обрабатывать аргументы, о которых он знает, и передавать остальное другим. Попытайтесь понять входы и выходы этого метода.
Мы говорили о композитных Namespace
объекты в более ранних ТАК вопросах. По умолчанию argparse.Namespace
класс является простым классом объекта с repr
метод. Парсер просто использует hasattr
, getattr
а также setattr
пытаясь быть настолько неспецифичным, насколько это возможно. Вы можете создать более сложный класс пространства имен.
подкоманды argparse с вложенными пространствами имен
Вы также можете настроить Action
классы. Вот где большинство значений вставляются в пространство имен (хотя значения по умолчанию установлены в другом месте).
IPython
использования argparse
, как для основного звонка, так и внутри для magic
команды. Он строит много аргументов из config
файлы. Таким образом, можно установить много значений либо с помощью стандартных настроек, пользовательских настроек, либо в последний момент с помощью аргументов командной строки.
Возможно, вы сможете использовать концепцию составления действий для достижения необходимой вам функциональности. Вы можете создавать действия, которые изменяют пространство имен, dest и т. Д. По мере необходимости, а затем составлять их с помощью:
def compose_actions(*actions):
"""Compose many argparse actions into one callable action.
Args:
*actions: The actions to compose.
Returns:
argparse.Action: Composed action.
"""
class ComposableAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
for action in actions:
action(option_string, self.dest).__call__(parser,
namespace,
values,
option_string)
return ComposableAction
Смотрите пример: https://gist.github.com/mnm364/edee068a5cebbfac43547b57b7c842f1