问题
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