Python argparse: список отдельных вариантов использования

У меня есть программа, которая принимает несколько аргументов, например

breakfast.py --customer=vikings eggs sausage bacon

где "яйца", "колбаса" и "бекон" могут быть указаны из списка конкретных вариантов.

Теперь мне нравится вывод breakfast.py --help выглядеть так:

usage: breakfast.py [-h] [--customer CUSTOMER] INGREDIENT [INGREDIENT ...]

positional arguments:
  your choice of ingredients:
    bacon              Lovely bacon
    egg                The runny kind
    sausage            Just a roll
    spam               Glorious SPAM
    tomato             Sliced and diced

optional arguments:
  -h, --help           show this help message and exit
  --customer CUSTOMER  salutation for addressing the customer

Я попробовал два подхода, но пока оба потерпели неудачу для меня.

Используя выбор аргумента:

import argparse

parser = argparse.ArgumentParser()

toppings = {
    'bacon': "Lovely bacon",
    'egg': 'The runny kind',
    'sausage': 'Just a roll',
    'spam': 'Glorious SPAM',
    'tomato': 'Sliced and diced',
}
parser.add_argument('--customer', default='Mr. and Mrs. Bun', action='store',
                    help='salutation for addressing the customer')
parser.add_argument('ingredients', nargs='+', choices=toppings.keys(),
                    help='your choice of ingredients')

options = parser.parse_args('--customer=Vikings egg sausage bacon'.split())
print("Dear {}, we are happy to serve you {}" \
      .format(options.customer, ', '.join(options.ingredients)))

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

usage: breakfast.py [-h] [--customer CUSTOMER]
                      {bacon,egg,sausage,spam,tomato}
                      [{bacon,egg,sausage,spam,tomato} ...]

positional arguments:
  {bacon,egg,sausage,spam,tomato}
                        your choice of ingredients

Добавление metavar='INGREDIENT' в add_argument('ingredients', ...) не перечисляет варианты вообще:

usage: breakfast.py [-h] [--customer CUSTOMER] INGREDIENT [INGREDIENT ...]

positional arguments:
  INGREDIENT           your choice of ingredients

Я кратко попытался использовать подпрограммы:

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('--customer', default='Mr. and Mrs. Bun', action='store',
                    help='salutation for addressing the customer')
ingredients = parser.add_subparsers(title='your choice of an ingredient',
                    dest='ingredient', metavar='ingredient')
ingredients.add_parser('bacon', help="Lovely bacon")
ingredients.add_parser('egg', help="The runny kind")
ingredients.add_parser('sausage', help="Just a roll")
ingredients.add_parser('spam', help="Glorious SPAM")
ingredients.add_parser('tomato', help="Sliced and diced")

options = parser.parse_args('--customer=Vikings spam'.split())
print("Dear {}, we are happy to serve you {}" \
      .format(options.customer, options.ingredient))

Который перечисляет использование так, как мне нравится:

usage: breakfast.py [-h] [--customer CUSTOMER] ingredient ...

optional arguments:
  -h, --help           show this help message and exit
  --customer CUSTOMER  salutation for addressing the customer

your choice of an ingredient:
  ingredient
    bacon              Lovely bacon
    egg                The runny kind
    sausage            Just a roll
    spam               Glorious SPAM
    tomato             Sliced and diced

По умолчанию подпрограммы позволяют выбирать только одну опцию. К счастью, этот ответ показывает, что можно разрешить несколько подкоманд), но это похоже на хак только для правильного форматирования. Недавно я перешел из argparse в ConfigArgParse, и этот подход потерпел неудачу.

Я думаю, что лучше вернуться к использованию одного аргумента с несколькими вариантами выбора и использовать обычное форматирование.

К сожалению, документации по настройке форматирования argparse недостаточно, поэтому я ценю некоторую помощь, как подойти к этому.

3 ответа

Основываясь на отзывах, я погрузился в код argparse. Разумное решение с использованием подпарсеров размещено по адресу /questions/29426514/kak-proanalizirovat-neskolko-vlozhennyih-podkomand-ispolzuya-python-argparse/29426523#29426523.

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

Первое решение определяет настраиваемое действие, цель которого - вообще ничего не делать, но при этом выводит некоторую информацию об использовании. Различные варианты даны этому классу NoAction.

import argparse

class NoAction(argparse.Action):
    def __init__(self, **kwargs):
        kwargs.setdefault('default', argparse.SUPPRESS)
        kwargs.setdefault('nargs', 0)
        super(NoAction, self).__init__(**kwargs)
    def __call__(self, parser, namespace, values, option_string=None):
        pass

parser = argparse.ArgumentParser()
parser.register('action', 'none', NoAction)

parser.add_argument('--customer', default='Mr. and Mrs. Bun', action='store',
                 help='salutation for addressing the customer')
parser.add_argument('ingredients', nargs='*', metavar='INGREDIENT',
                 choices=['bacon', 'egg', 'sausage', 'spam', 'tomato'],
                 help='List of ingredients')

group = parser.add_argument_group(title='your choice of ingredients')
group.add_argument('bacon', help="Lovely bacon", action='none')
group.add_argument('egg', help="The runny kind", action='none')
group.add_argument('sausage', help="Just a roll", action='none')
group.add_argument('spam', help="Glorious SPAM", action='none')
group.add_argument('tomato', help="Sliced and diced", action='none')

options = parser.parse_args('--customer=Vikings egg sausage bacon'.split())
print("Dear {}, we are happy to serve you {}" \
      .format(options.customer, ', '.join(options.ingredients)))

options = parser.parse_args(['--help'])

какие выводы:

Dear Vikings, we are happy to serve you egg, sausage, bacon

usage: customchoices.py [-h] [--customer CUSTOMER]
                        [INGREDIENT [INGREDIENT ...]]

positional arguments:
  INGREDIENT           List of ingredients

optional arguments:
  -h, --help           show this help message and exit
  --customer CUSTOMER  salutation for addressing the customer

your choice of ingredients:
  bacon                Lovely bacon
  egg                  The runny kind
  sausage              Just a roll
  spam                 Glorious SPAM
  tomato               Sliced and diced

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

import argparse

class NoAction(argparse.Action):
    def __init__(self, **kwargs):
        kwargs.setdefault('default', argparse.SUPPRESS)
        kwargs.setdefault('nargs', 0)
        super(NoAction, self).__init__(**kwargs)
    def __call__(self, parser, namespace, values, option_string=None):
        pass

class ChoicesAction(argparse._StoreAction):
    def add_choice(self, choice, help=''):
        if self.choices is None:
            self.choices = []
        self.choices.append(choice)
        self.container.add_argument(choice, help=help, action='none')

parser = argparse.ArgumentParser()
parser.register('action', 'none', NoAction)
parser.register('action', 'store_choice', ChoicesAction)

parser.add_argument('--customer', default='Mr. and Mrs. Bun', action='store',
                 help='salutation for addressing the customer')

group = parser.add_argument_group(title='your choice of ingredients')
ingredients = group.add_argument('ingredients', nargs='*', metavar='INGREDIENT',
                 action='store_choice')
ingredients.add_choice('bacon', help="Lovely bacon")
ingredients.add_choice('egg', help="The runny kind")
ingredients.add_choice('sausage', help="Just a roll")
ingredients.add_choice('spam', help="Glorious SPAM")
ingredients.add_choice('tomato', help="Sliced and diced")

Выше, наверное, мой любимый метод, несмотря на два подкласса действий. Он использует только публичные методы.

Альтернативой является модификация Formatter. Это возможно, это изменяет action.choices из списка ['option1', 'option2'] диктовать {'option1': 'help_for_option1', 'option2', 'help_for_option2'}и более-менее реализует HelpFormatter._format_action() как HelpFormatterWithChoices.format_choices():

import argparse

class HelpFormatterWithChoices(argparse.HelpFormatter):
    def add_argument(self, action):
        if action.help is not argparse.SUPPRESS:
            if isinstance(action.choices, dict):
                for choice, choice_help in action.choices.items():
                    self._add_item(self.format_choices, [choice, choice_help])
            else:
                super(HelpFormatterWithChoices, self).add_argument(action)
    def format_choices(self, choice, choice_help):
        # determine the required width and the entry label
        help_position = min(self._action_max_length + 2,
                            self._max_help_position)
        help_width = max(self._width - help_position, 11)
        action_width = help_position - self._current_indent - 2
        choice_header = choice

        # short choice name; start on the same line and pad two spaces
        if len(choice_header) <= action_width:
            tup = self._current_indent, '', action_width, choice_header
            choice_header = '%*s%-*s  ' % tup
            indent_first = 0

        # long choice name; start on the next line
        else:
            tup = self._current_indent, '', choice_header
            choice_header = '%*s%s\n' % tup
            indent_first = help_position

        # collect the pieces of the choice help
        parts = [choice_header]

        # add lines of help text
        help_lines = self._split_lines(choice_help, help_width)
        parts.append('%*s%s\n' % (indent_first, '', help_lines[0]))
        for line in help_lines[1:]:
            parts.append('%*s%s\n' % (help_position, '', line))

        # return a single string
        return self._join_parts(parts)

parser = argparse.ArgumentParser(formatter_class=HelpFormatterWithChoices)

toppings = {
    'bacon': "Lovely bacon",
    'egg': 'The runny kind',
    'sausage': 'Just a roll',
    'spam': 'Glorious SPAM',
    'tomato': 'Sliced and diced',
}

parser.add_argument('--customer', default='Mr. and Mrs. Bun', action='store',
                 help='salutation for addressing the customer')

group = parser.add_argument_group(title='your choice of ingredients')
ingredients = group.add_argument('ingredients', nargs='*', metavar='INGREDIENT',
                 choices=toppings)

options = parser.parse_args('--customer=Vikings egg sausage bacon'.split())
print("Dear {}, we are happy to serve you {}" \
      .format(options.customer, ', '.join(options.ingredients)))

print()
options = parser.parse_args(['--help'])

какие выводы:

Dear Vikings, we are happy to serve you egg, sausage, bacon

usage: helpformatter.py [-h] [--customer CUSTOMER]
                        [INGREDIENT [INGREDIENT ...]]

optional arguments:
  -h, --help           show this help message and exit
  --customer CUSTOMER  salutation for addressing the customer

your choice of ingredients:
  bacon                Lovely bacon
  egg                  The runny kind
  sausage              Just a roll
  spam                 Glorious SPAM
  tomato               Sliced and diced

Следует отметить, что это единственный подход, который не печатает строку помощи для самих "ИНГРЕДИЕНТОВ", а только варианты.

Тем не менее, мне не нравится этот подход: он повторно реализует слишком много кода и полагается на слишком много внутренних деталей реализации argparse.

Возможен также гибридный подход: код подпункта в argparser использует собственность action._choices_actions, Это обычно в _SubParsersAction класс, как для разбора, так и для форматирования. Что если мы используем это свойство, но только для форматирования?

import argparse

class ChoicesAction(argparse._StoreAction):
    def __init__(self, **kwargs):
        super(ChoicesAction, self).__init__(**kwargs)
        if self.choices is None:
            self.choices = []
        self._choices_actions = []
    def add_choice(self, choice, help=''):
        self.choices.append(choice)
        # self.container.add_argument(choice, help=help, action='none')
        choice_action = argparse.Action(option_strings=[], dest=choice, help=help)
        self._choices_actions.append(choice_action)
    def _get_subactions(self):
        return self._choices_actions

parser = argparse.ArgumentParser()
parser.register('action', 'store_choice', ChoicesAction)

parser.add_argument('--customer', default='Mr. and Mrs. Bun', action='store',
                 help='salutation for addressing the customer')

group = parser.add_argument_group(title='your choice of ingredients')
ingredients = group.add_argument('ingredients', nargs='*', metavar='INGREDIENT',
                 action='store_choice')
ingredients.add_choice('bacon', help="Lovely bacon")
ingredients.add_choice('egg', help="The runny kind")
ingredients.add_choice('sausage', help="Just a roll")
ingredients.add_choice('spam', help="Glorious SPAM")
ingredients.add_choice('tomato', help="Sliced and diced")

options = parser.parse_args('--customer=Vikings egg sausage bacon'.split())
print("Dear {}, we are happy to serve you {}" \
      .format(options.customer, ', '.join(options.ingredients)))

print()
options = parser.parse_args(['--help'])

какие выводы:

Dear Vikings, we are happy to serve you egg, sausage, bacon

usage: helpformatter2.py [-h] [--customer CUSTOMER]
                         [INGREDIENT [INGREDIENT ...]]

optional arguments:
  -h, --help           show this help message and exit
  --customer CUSTOMER  salutation for addressing the customer

your choice of ingredients:
  INGREDIENT
    bacon              Lovely bacon
    egg                The runny kind
    sausage            Just a roll
    spam               Glorious SPAM
    tomato             Sliced and diced

Это также хорошее решение, хотя оно основывается на деталях реализации _get_subactions() метод.

Изменение add_argument чтобы:

parser.add_argument('ingredients', nargs='+', choices=toppings.keys(),
                metavar='INGREDIENT',
                help='your choice of ingredients: %(choices)s')

производит

usage: stack49969605.py [-h] [--customer CUSTOMER] INGREDIENT [INGREDIENT ...]

positional arguments:
  INGREDIENT           your choice of ingredients: bacon, egg, sausage, spam,
                       tomato

Смена форматера:

formatter_class=argparse.RawTextHelpFormatter

и add_argumenthelp параметр для:

                    help = """
your choice of ingredients:
bacon              Lovely bacon
egg                The runny kind
sausage            Just a roll
spam               Glorious SPAM
tomato             Sliced and diced
    """
    )

производит:

usage: stack49969605.py [-h] [--customer CUSTOMER] INGREDIENT [INGREDIENT ...]

positional arguments:
  INGREDIENT           
                       your choice of ingredients:
                       bacon              Lovely bacon
                       egg                The runny kind
                       sausage            Just a roll
                       spam               Glorious SPAM
                       tomato             Sliced and diced

Или вы могли бы использовать argparse.RawDescriptionHelpFormatterи поместите отформатированную таблицу в description или же epilog,


Другим вариантом является создание подкласса Action, который имитирует аспекты subparsers учебный класс. Но это требует более глубокого понимания того, как этот класс Action обрабатывается при форматировании.

class _SubParsersAction(Action):
    class _ChoicesPseudoAction(Action):

subparsers Объект действия поддерживает список _choices_actions этого PseudoAction класс исключительно для того, чтобы обмануть средство форматирования справки для отображения подпарсеров, как если бы они были группой вложенных Actions, Этот список не используется для анализа; только для справки форматирования.

Почему бы просто не разобрать аргументы самостоятельно, не используя argparser? Тогда вы сможете свободно форматировать экран справки так, как вам нравится.

import sys

 if sys.argv[1] in ['-h','--help']:
     print "usage: breakfast.py [-h] [--customer CUSTOMER] INGREDIENT [INGREDIENT ...]\n\npositional arguments:\n\tyour choice of ingredients:\n\t\tbacon              Lovely bacon\n\t\tegg                The runny kind\n\t\tsausage            Just a roll\n\t\tspam               Glorious SPAM\n\t\ttomato             Sliced and diced\n\noptional arguments:\n\t-h, --help           show this help message and exit\n\t--customer CUSTOMER  salutation for addressing the customer"

customer_arg = sys.argv[1]
ingrediants = sys.argv[2:len(sys.argv)]
customer = customer_arg.split('=')[1]

Это напечатает:

usage: breakfast.py [-h] [--customer CUSTOMER] INGREDIENT [INGREDIENT ...]

positional arguments:
        your choice of ingredients:
                bacon              Lovely bacon
                egg                The runny kind
                sausage            Just a roll
                spam               Glorious SPAM
                tomato             Sliced and diced

optional arguments:
        -h, --help           show this help message and exit
        --customer CUSTOMER  salutation for addressing the customer

Затем вы можете делать все, что будет дальше со списком ингредиентов. Надеюсь, это поможет.

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