Python argparse: list individual choices in the usage

二次信任 提交于 2019-12-23 04:50:12

问题


I have a program which takes multiple arguments, e.g.

breakfast.py --customer=vikings eggs sausage bacon

where "eggs", "sausage" and "bacon" can be specified from a list of specific choices.

Now I like the output of breakfast.py --help to look like this:

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

I tried two approaches, but so far both failed for me.

Using argument choices:

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)))

The usage of the above program prints a dict-formated list without details:

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

Adding metavar='INGREDIENT' to add_argument('ingredients', ...) does not list the choices at all:

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

positional arguments:
  INGREDIENT           your choice of ingredients

I briefly tried to use subprograms:

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))

Which does list the usage in the way I like it:

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

By default, subprograms only allow one options to be picked. Fortunately this answer shows it is possible to allow multiple subcommands), but this feels like a hack just to get the formatting right. I recently moved from argparse to ConfigArgParse, and this approach failed there.

I think I better revert to using a single argument with multiple choices, and use customat formatting.

Unfortunately, the documentation on adjusting the formatting of argparse is scarce, so I appreciate some help how to approach this.


回答1:


Changing the add_argument to:

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

produces

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

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

Changing formatter:

formatter_class=argparse.RawTextHelpFormatter

and the add_argument help parameter to:

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

produces:

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

Or you could use argparse.RawDescriptionHelpFormatter, and put the formatted table in the description or epilog.


Another option is to make an Action subclass that imitates aspects of the subparsers class. But that requires a deeper understanding of how this Action class is handled in the formatting.

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

The subparsers Action object maintains a list of _choices_actions of this PseudoAction class solely for the purpose of tricking the help formatter into displaying the subparsers as though they were a group of nested Actions. This list is not used for parsing; only for help formatting.




回答2:


Based on the feedback here, I dived into the argparse code. A reasonable solution that uses subparsers is posted at https://stackoverflow.com/a/49977713/428542.

In addition, I was able to find a solution that added a pseudo-action for each option, as well a solution that modified the formatter. Finally I present a hybrid solution that adds pseudo-action for each option, but in such a way that only the formatter uses them, by exploiting some implementation details.

The first solution defines a custom action, whose purpose is to do nothing at all, but still prints some usage information. The different options are given this NoAction class.

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'])

which outputs:

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

A minor disadvantage is that the individual choices are both added to the ingredients (for parsing) as well as to the parser (for formatting). We could also define a method to add the choices to the ingredients parser directly:

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")

The above is probably my favourite method, despite the two action subclasses. It only uses public methods.

An alternative is to modify the Formatter. This is possible, it modifies action.choices from a list ['option1', 'option2'] to a dict {'option1': 'help_for_option1', 'option2', 'help_for_option2'}, and more-or-less re-implements HelpFormatter._format_action() as 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'])

which outputs:

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

It should be noted that this is the only approach that does not print a help line for "INGREDIENTS" itself, but only the choices.

Nevertheless, I dislike this approach: it re-implements too much code, and relies on too much internal implementation details of argparse.

There is also a hybrid approach possible: the subparser code in argparser makes use of a property action._choices_actions. This is normally in the _SubParsersAction class, both for parsing and for formatting. What if we use this property, but only for formatting?

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'])

which outputs:

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

This is also a nice solution, although it relies on the implementation detail of the _get_subactions() method.




回答3:


Why not just parse the arguments yourself without using argparser? Then you could have all the freedom in formatting the help screen just the way that you like.

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]

This will print:

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

Then you can do whatever's next with the ingrediant list. I hope this helps.



来源:https://stackoverflow.com/questions/49969605/python-argparse-list-individual-choices-in-the-usage

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!