Условные аргументы Python Argparse
Я сделал столько исследований, насколько это возможно, но я не нашел лучшего способа сделать некоторые аргументы cmdline необходимыми только при определенных условиях, в этом случае, только если были даны другие аргументы. Вот что я хочу сделать на очень базовом уровне:
p = argparse.ArgumentParser(description='...')
p.add_argument('--argument', required=False)
p.add_argument('-a', required=False) # only required if --argument is given
p.add_argument('-b', required=False) # only required if --argument is given
Из того, что я видел, другие люди, кажется, просто добавляют свою собственную проверку в конце:
if args.argument and (args.a is None or args.b is None):
# raise argparse error here
Есть ли способ сделать это изначально внутри пакета argparse?
Ответы
Ответ 1
Я искал простой ответ на этот вопрос в течение некоторого времени. Все, что вам нужно сделать, это проверить, находится ли '--argument'
в sys.argv
, поэтому в основном для вашего образца кода, который вы могли бы просто сделать:
import argparse
import sys
if __name__ == '__main__':
p = argparse.ArgumentParser(description='...')
p.add_argument('--argument', required=False)
p.add_argument('-a', required='--argument' in sys.argv) #only required if --argument is given
p.add_argument('-b', required='--argument' in sys.argv) #only required if --argument is given
args = p.parse_args()
Таким образом required
получает либо True
, либо False
в зависимости от того, используется ли пользователь --argument
. Уже протестировано, похоже, работает и гарантирует, что -a
и -b
имеют независимое поведение между собой.
Ответ 2
Вы можете реализовать проверку, предоставив настраиваемое действие для --argument
, которое примет дополнительный аргумент ключевого слова, чтобы указать, какие другие действия станут необходимыми, если используется --argument
.
import argparse
class CondAction(argparse.Action):
def __init__(self, option_strings, dest, nargs=None, **kwargs):
x = kwargs.pop('to_be_required', [])
super(CondAction, self).__init__(option_strings, dest, **kwargs)
self.make_required = x
def __call__(self, parser, namespace, values, option_string=None):
for x in self.make_required:
x.required = True
try:
return super(CondAction, self).__call__(parser, namespace, values, option_string)
except NotImplementedError:
pass
p = argparse.ArgumentParser()
x = p.add_argument("--a")
p.add_argument("--argument", action=CondAction, to_be_required=[x])
Точное определение CondAction
будет зависеть от того, что именно следует делать --argument
. Но, например, если --argument
является регулярным типом действия take-one-argument-and-save-it, то достаточно наследовать от argparse._StoreAction
должно быть достаточно.
В примере анализатора мы сохраняем ссылку на параметр --a
внутри параметра --argument
, и когда --argument
отображается в командной строке, он устанавливает флаг required
на --a
на True
. После того, как все параметры обработаны, argparse
проверяет, что была выбрана любая опция, отмеченная как необходимая.
Ответ 3
Тест на анализ сообщений отлично, особенно если тестирование по умолчанию с is None
соответствует вашим потребностям.
http://bugs.python.org/issue11588 'Add "necessarily inclusive" groups to argparse'
рассматривает реализацию таких тестов, используя механизм groups
(обобщение mutuall_exclusive_groups).
Я написал набор UsageGroups
, который реализует тесты типа xor
(взаимоисключающие), and
, or
и not
. Я думал, что те, где они полны, но я не смог выразить ваше дело с точки зрения этих операций. (похоже, мне нужно nand
- нет и, см. ниже)
Этот script использует пользовательский класс Test
, который по существу реализует ваш тест после анализа. seen_actions
- это список действий, которые видел синтаксический анализ.
class Test(argparse.UsageGroup):
def _add_test(self):
self.usage = '(if --argument then -a and -b are required)'
def testfn(parser, seen_actions, *vargs, **kwargs):
"custom error"
actions = self._group_actions
if actions[0] in seen_actions:
if actions[1] not in seen_actions or actions[2] not in seen_actions:
msg = '%s - 2nd and 3rd required with 1st'
self.raise_error(parser, msg)
return True
self.testfn = testfn
self.dest = 'Test'
p = argparse.ArgumentParser(formatter_class=argparse.UsageGroupHelpFormatter)
g1 = p.add_usage_group(kind=Test)
g1.add_argument('--argument')
g1.add_argument('-a')
g1.add_argument('-b')
print(p.parse_args())
Образец вывода:
1646:~/mypy/argdev/usage_groups$ python3 issue25626109.py --arg=1 -a1
usage: issue25626109.py [-h] [--argument ARGUMENT] [-a A] [-b B]
(if --argument then -a and -b are required)
issue25626109.py: error: group Test: argument, a, b - 2nd and 3rd required with 1st
usage
и сообщения об ошибках по-прежнему нуждаются в работе. И это не делает ничего такого, что тест после разбора не может.
Ваш тест вызывает ошибку, если (argument & (!a or !b))
. И наоборот, разрешено !(argument & (!a or !b)) = !(argument & !(a and b))
. Добавив тест nand
в мои классы UsageGroup
, я могу реализовать ваш случай как:
p = argparse.ArgumentParser(formatter_class=argparse.UsageGroupHelpFormatter)
g1 = p.add_usage_group(kind='nand', dest='nand1')
arg = g1.add_argument('--arg', metavar='C')
g11 = g1.add_usage_group(kind='nand', dest='nand2')
g11.add_argument('-a')
g11.add_argument('-b')
Использование (с помощью !()
для отметки теста 'nand'):
usage: issue25626109.py [-h] !(--arg C & !(-a A & -b B))
Я думаю, что это самый короткий и ясный способ выразить эту проблему с использованием групп общего назначения.
В моих тестах входы, которые успешно выполняются,:
''
'-a1'
'-a1 -b2'
'--arg=3 -a1 -b2'
Предполагается, что сообщения, вызывающие ошибки, следующие:
'--arg=3'
'--arg=3 -a1'
'--arg=3 -b2'
Ответ 4
До тех пор, пока http://bugs.python.org/issue11588 не будет решена, я просто использую nargs
:
p = argparse.ArgumentParser(description='...')
p.add_argument('--arguments', required=False, nargs=2, metavar=('A', 'B'))
Таким образом, если кто-то поставляет --arguments
, он будет иметь 2 значения.
Возможно, его результат CLI менее читабельен, но код намного меньше. Вы можете исправить это с помощью хорошего документа/справки.
Ответ 5
Это действительно то же самое, что и ответ @Mira, но я хотел показать его для случая, когда при задании опции требуется дополнительный аргумент:
Например, если --option foo
то требуются также некоторые аргументы, которые не требуются, если --option bar
:
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--option', required=True,
help='foo and bar need different args')
if 'foo' in sys.argv:
parser.add_argument('--foo_opt1', required=True,
help='--option foo requires "--foo_opt1"')
parser.add_argument('--foo_opt2', required=True,
help='--option foo requires "--foo_opt2"')
...
if 'bar' in sys.argv:
parser.add_argument('--bar_opt', required=True,
help='--option bar requires "--bar_opt"')
...
Он не идеален - например, proggy --option foo --foo_opt1 bar
неоднозначна, но для того, что мне нужно было сделать, все в порядке.
Ответ 6
Для аргументов я придумал быстрое n-грязное решение вроде этого.
Предположения: (1) "-help" должен отображать справку и не жаловаться на требуемый аргумент, и (2) мы анализируем sys.argv
p = argparse.ArgumentParser(...)
p.add_argument('-required', ..., required = '--help' not in sys.argv )
Это можно легко изменить в соответствии с конкретными настройками.
Для требуемых позиционных элементов (которые не будут запрашиваться, если в командной строке указано "-help" ), я пришел к следующему: [positionals не позволяют использовать required=...
keyword arg!]
p.add_argument('pattern', ..., narg = '+' if '--help' not in sys.argv else '*' )
в основном это превращает количество требуемых вхождений "шаблона" в командной строке от одного или более к нулю или более в случае, если задан "-help" .