Иметь парсер верхнего уровня и субпарсеры, работающие с одной и той же переменной

у меня есть ArgumentParser с подпарсерами. Некоторые флаги являются общими для всех подкоманд, и хотелось бы иметь возможность указывать их либо до, либо после подкоманды, либо даже смешивать до и после (на усмотрение пользователя).

Что-то вроде этого:

$ ./test -v
Namespace(v=1)
$ ./test.py test -vv
Namespace(v=2)
$ ./test.py -vvv test
Namespace(v=3)
$ ./test.py -vv test -vv
Namespace(v=4)

Итак, я попробовал что-то вроде этого:

#!/usr/bin/env python3

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("-v", action = "count")

subparsers = parser.add_subparsers()
sub = subparsers.add_parser("test")
sub.add_argument("-v", action = "count")

print(parser.parse_args())

Запуск этой программы "test.py" дает мне:

 ./test.py -h
usage: test.py [-h] [-v] {test} ...

positional arguments:
  {test}

options:
  -h, --help  show this help message and exit
  -v

$ ./test.py test -h
usage: test.py test [-h] [-v]

options:
  -h, --help  show this help message and exit
  -v

$ ./test.py -v
Namespace(v=1)

$ ./test.py test -vv
Namespace(v=2)

прохладный.

но это также дает мне:

$ ./test.py -vvv test
Namespace(v=None)
$ ./test.py -vv test -vv
Namespace(v=2)

менее круто :-(

Я также попытался явно указать родительские парсеры:

common = argparse.ArgumentParser(add_help=False)
common.add_argument("-v", action = "count")

parser = argparse.ArgumentParser(parents=[common])
sub = parser.add_subparsers().add_parser("test", parents=[common])

print(parser.parse_args())

но результат тот же.

Итак, я предполагаю, что как только срабатывает субпарсер test, он сбрасывает значение if v на None.

Как мне это предотвратить?

(Я заметил, что Как я могу определить глобальные параметры с помощью подпарсеров в Python argparse? аналогично, и ответ там предлагает использовать разные dest переменные для парсера верхнего и подуровня. Я хотел бы избегайте этого...)

Как говорится в моем старом ответе, приоритет аргумента субпарсера (включая значение по умолчанию) был намеренным выбором (первоначальным автором). Разное dest позволяет вам выбрать, какое значение вы будете использовать — в коде синтаксического анализа сообщений. Переписывание подкласса Action субпарсера возможно для опытных программистов.

hpaulj 06.06.2024 17:25
parents — это всего лишь сокращенный способ определения действий; в противном случае он не делает ничего особенного. При указании одного и того же dest на основном и подуровне программа должна сделать какой-то выбор. Разработчики выбрали один вариант, который имеет смысл во многих, но не во всех случаях. count не получает специального лечения. Не обязательно делать все в пределах argparse. Пост-парсинг кода разрешен.
hpaulj 06.06.2024 20:12
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
2
56
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Желаемое поведение action='count' в соответствии с вопросом просто невозможно с использованием одного ArgumentParser (начиная с Python 3.12.3) из-за того, как подпространство имен в конце объединяется с основным пространством имен. В частности, внутренний вызываемый объект _CountAction попытается найти dest в текущем пространстве имен, установит для него значение 0, если оно еще не существует, и увеличит значение на 1. Однако внутренний вызываемый объект _SubparserAction просто наивно объединит полученные значения (не точная, но очень связанная известная проблема). Вы можете убедиться в этом, проследив выполнение с помощью следующего argparser (который настроен с дополнительными флагами по умолчанию для устранения неоднозначности вместе с примером входных данных) и пошагово пройдите через отладчик:

import argparse

common = argparse.ArgumentParser(add_help=False)
common.add_argument("-v", action = "count")
common.add_argument("-c", default = "common")

parser = argparse.ArgumentParser(parents=[common])
parser.add_argument("-p", default = "parser")
sub = parser.add_subparsers().add_parser("test", parents=[common])
sub.add_argument("-s", default = "sub")

args = ['-v', '-v', 'test', '-v', '-v']

# demo
print(f"parser: {parser.parse_args(args)}")

В качестве альтернативы шагу через отладчик вставьте print(namespace) над связанными строками для _CountAction.__call__ и print(f"{subnamespace} merging into {namespace}") перед блоком for во втором блоке кода, вы получите следующий вывод:

Namespace(v=None, c='common', p='parser')
Namespace(v=1, c='common', p='parser')
Namespace(v=None, c='common', s='sub')
Namespace(v=1, c='common', s='sub')
Namespace(v=2, c='common', s='sub') merging into Namespace(v=2, c='common', p='parser')
parser: Namespace(v=2, c='common', p='parser', s='sub')

Я работал над этой конкретной проблемой много лет назад в пакете, который я написал (в частности, этот коммит , который намекал на эту очень конкретную проблему), используя обычный синтаксический анализатор и parse_known_args с входящими аргументами. Поскольку в вашем последнем примере уже есть общий синтаксический анализатор, измените демонстрационную часть следующим образом:

print(f"common: {common.parse_known_args(args)}")
print(f"parser: {parser.parse_args(args)}")

Должен выдать следующий результат:

common: (Namespace(v=4, c='common'), ['test'])
parser: Namespace(v=2, c='common', p='parser', s='sub')

Выберите то, что вам нужно, из известных аргументов common (и игнорируйте остаток), проанализируйте список аргументов как обычно с помощью parser и используйте остальную часть как обычно.

это прекрасно работает... но теперь я столкнулся с проблемой: я также хочу иметь разные значения по умолчанию для каждой подкоманды (но, очевидно, это не было частью моего вопроса)

umläute 07.06.2024 19:39

Я думаю, проблема связана с тем, что теперь все субпарсеры используют одно и то же действие (которое имеет значение по умолчанию). однако идея использовать parse_known_args() для объединения флагов великолепна.

umläute 07.06.2024 19:48

Действия копируются по ссылке из родителя. Таким образом, индивидуальное изменение атрибутов, таких как default, невозможно. Родители — это просто удобный инструмент. С таким же успехом вы можете написать свою собственную служебную функцию для создания действий или использовать старое доброе копирование и вставку.

hpaulj 07.06.2024 23:03

Да, если значения по умолчанию разные, вам нужно будет устранить неоднозначность с помощью другого dest, потому что невозможно обойти это, используя остаток parse_known_args на манекене, потому что учтите, что остаток содержит только ['test'] (а не ['test', '-v', ...], т. е. с конечными аргументами), и что не существует других доступных методов частичного или итеративного анализа.

metatoaster 12.06.2024 12:39

Я знаю, что вы хотите избежать разных dest (почему?), но это кажется самым простым подходом. Это дает вам максимальный контроль.

In [4]: import argparse
   ...: 
   ...: parser = argparse.ArgumentParser()
   ...: parser.add_argument("-v", action = "count", dest='foo', default=0)
   ...: 
   ...: subparsers = parser.add_subparsers()
   ...: sub = subparsers.add_parser("test")
   ...: sub.add_argument("-v", action = "count", dest='subfoo', default=0);

In [5]: args = parser.parse_args(['-vv', 'test']); args
Out[5]: Namespace(foo=2, subfoo=0)

In [6]: args.count= args.foo+args.subfoo; args
Out[6]: Namespace(foo=2, subfoo=0, count=2)

In [7]: args = parser.parse_args(['test', '-vvv']); args
Out[7]: Namespace(foo=0, subfoo=3)

In [8]: args.count= args.foo+args.subfoo; args
Out[8]: Namespace(foo=0, subfoo=3, count=3)

In [9]: args = parser.parse_args(['-v','test', '-vvv']); args
Out[9]: Namespace(foo=1, subfoo=3)

In [10]: args.count= args.foo+args.subfoo; args
Out[10]: Namespace(foo=1, subfoo=3, count=4)

Как отмечалось ранее, значение subparser имеет приоритет. При вызове subparser получает новый Namespace, а не наследует тот, который использовался основными парсерами. По возвращении это подпространство имен используется для обновления основного пространства имен. Есть аргументы за и против этого выбора.

Выбирая разные dest, вы сохраняете контроль над тем, какой парсер имеет приоритет. Или в случае такого действия, как count способность относиться к ним одинаково. Стрелять, ты бы даже смог args.diff = args.foo - args.subfoo!

Я хотел бы избежать различных dest, так как это требует ручной обработки всех параметров (снова). на практике мое решение делает что-то вроде этого (используя добавление тире (-v) в качестве dest для флага в парсере верхнего уровня, поэтому я могу легко сопоставить значение поданализатора со значением верхнего уровня, не сталкиваясь с опасностью конфликтов имен ; а затем автоматически объединить их); но это немного некрасиво.

umläute 07.06.2024 19:45

Для полноты картины я публикую свое фактическое решение, которое в значительной степени основано на принятом ответе:

def add_common_args(parser, defaults = {}):
    parser.add_argument("-v", "--verbose" action = "count")
    parser.add_argument("--foo")
    parser.set_defaults(**defaults)

common = argparse.ArgumentParser(add_help=False)
add_common_args(common)

parser = argparse.ArgumentParser(parents=[common])
subparsers = parser.add_subparsers()
p = subparsers.add_parser(subparsers, "test")
add_common_args(p, defaults_test)
p.add_argument("--bar")

# parse common args and subparser args separately
common_args = common.parse_known_args()[0]
args = parser.parse_args()

# fix common args
for k, v in common_args._get_kwargs():
    if v is None:
        continue
    setattr(args, k, v)

Хитрость заключается в том, чтобы (как предложил метатоастер) выполнить частичный анализ, который распознает только общие аргументы (игнорируя все неизвестное, например, сам субпарсер и конкретные аргументы субпарсера; но накапливая флаги count и гарантируя, что для обычных флагов побеждает последний).

Таким образом, прохождение -v --foo A test -v --foo B приведет к Namespace(v=2, foo='B', bar=None).

Чтобы добиться значений по умолчанию для каждого подпарсера (которые можно прочитать из файла конфигурации и, следовательно, они неизвестны), общие аргументы реализуются с помощью служебной функции add_common_args() (согласно предложению hpaulj), а не совместного использования common синтаксического анализатора. Нам нужно выделить неустановленные значения в особом случае из парсера common (if v is None: continue), чтобы значение по умолчанию не было перезаписано неустановленным значением.

Другие вопросы по теме