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.