Python argparse: Lots of choices results in ugly help output

PythonArgparse

Python Problem Overview


I have this code which I am generally pleased with:

import argparse

servers = [ "ApaServer", "BananServer", "GulServer", "SolServer", "RymdServer",
            "SkeppServer", "HavsServer", "PiratServer", "SvartServer", "NattServer", "SovServer" ]

parser = argparse.ArgumentParser(description="A program to update components on servers.")
group = parser.add_mutually_exclusive_group()
group.add_argument('-l', '--list', dest="update", action='store_false', default=False, help='list server components')
group.add_argument('-u', '--updatepom', dest="update", action='store_true', help='update server components')
parser.add_argument('-o', '--only', nargs='*', choices=servers, help='Space separated list of case sensitive server names to process')
parser.add_argument('-s', '--skip', nargs='*', choices=servers, help='Space separated list of case sensitive server names to exclude from processing')
args = parser.parse_args()

I like that the choice=servers validates the server names in the input for me, so that I won´t have to. However, having so many valid choices makes the help output look terrible:

usage: args.py [-h] [-l | -u]
               [-o [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]]]
               [-s [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]]]

A program to update components on servers.

optional arguments:
  -h, --help            show this help message and exit
  -l, --list            list server components
  -u, --updatepom       update server components
  -o [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]], --only [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]]
                        Space separated list of case sensitive server names to
                        process
  -s [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]], --skip [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]]
                        Space separated list of case sensitive server names to
                        exclude from processing

Which way would you recommend if I want:

  • Nice (mostly) auto-generated help output
  • Validation that the entries given to the -o or the -s options are in servers.

Bonus:

  • Would it be possible to have case insensitive string matching for the server names?

Append

I tried using michaelfilms suggestion where the -o -s options are removed from the above output and this part is added:

server optional arguments:
  Valid server names are: ApaServer, BananServer, GulServer, SolServer,
  RymdServer, SkeppServer, HavsServer, PiratServer, SvartServer,
  NattServer, SovServer

I think it looks pretty good, but I really need to provide help for -o and -s options as the user wouldn´t know about them otherwise. So I am not all the way there yet using this approach.

Python Solutions


Solution 1 - Python

I am basically repeating what Ernest said - to avoid the ugly long list of choices, set metavar='' for the choice-based arguments (though it won't get rid of the whitespace between the argument and comma (e.g. -o , instead of -o,). You can then describe available choices in detail in general description (RawDescriptionHelpFormatter is useful here if you want them listed with obvious indentation).

I do not understand why Ernest's answer was voted down. This code

import argparse

servers = [ "ApaServer", "BananServer", "GulServer", "SolServer", "RymdServer",
            "SkeppServer", "HavsServer", "PiratServer", "SvartServer", "NattServer", "SovServer" ]

parser = argparse.ArgumentParser(description="A program to update components on servers.")
group = parser.add_mutually_exclusive_group()
group.add_argument('-l', '--list', dest="update", action='store_false', default=False, help='list server components')
group.add_argument('-u', '--updatepom', dest="update", action='store_true', help='update server components')
parser.add_argument('-o', '--only', choices=servers, help='Space separated list of case sensitive server names to process.  Allowed values are '+', '.join(servers), metavar='')
parser.add_argument('-s', '--skip', choices=servers, help='Space separated list of case sensitive server names to exclude from processing.  Allowed values are '+', '.join(servers), metavar='')
args = parser.parse_args()

produces the following help output

usage: run.py [-h] [-l | -u] [-o] [-s]

A program to update components on servers.

optional arguments:
  -h, --help       show this help message and exit
  -l, --list       list server components
  -u, --updatepom  update server components
  -o , --only      Space separated list of case sensitive server names to
                   process. Allowed values are ApaServer, BananServer,
                   GulServer, SolServer, RymdServer, SkeppServer, HavsServer,
                   PiratServer, SvartServer, NattServer, SovServer
  -s , --skip      Space separated list of case sensitive server names to
                   exclude from processing. Allowed values are ApaServer,
                   BananServer, GulServer, SolServer, RymdServer, SkeppServer,
                   HavsServer, PiratServer, SvartServer, NattServer, SovServer

This is hopefully what original post was looking for.

Solution 2 - Python

There is no need to subclass anything. Simply pass a metavar argument with the string you want to appear in the help message.

See the argparse documentation for details.

Solution 3 - Python

I have this same problem, and as a workaround, I used the epilog to describe each of the option choices. I had to use argparse.RawTextHelpFormatter, which lets you specify that the epilog is pre-formatted.

def choicesDescriptions():
return """
Choices supports the following:
choice1         - the FIRST option
choice2         - the SECOND option
...
choiceN         - the Nth option
"""




def getChoices():
return ["choice1", "choice2", ..., "choiceN"]




parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, epilog=choicesDescriptions())
parser.add_argument(
'choices',
choices=getChoices(),
help='Arg choice.  See the choices options below'
)




args = parser.parse_args()
print(args)

args = parser.parse_args() print(args)

Solution 4 - Python

This will not help in situations where the options list is extremely long, as in the original question, but for those people who, like me, came across this question looking for a way to break moderately long options strings into two lines, here is my solution:

import argparse

class CustomFormatter(argparse.HelpFormatter):
    """Custom formatter for setting argparse formatter_class. Identical to the
    default formatter, except that very long option strings are split into two
    lines.
    """
    
    def _format_action_invocation(self, action):
        if not action.option_strings:
            metavar, = self._metavar_formatter(action, action.dest)(1)
            return metavar
        else:
            parts = []
            # if the Optional doesn't take a value, format is:
            #    -s, --long
            if action.nargs == 0:
                parts.extend(action.option_strings)
            # if the Optional takes a value, format is:
            #    -s ARGS, --long ARGS
            else:
                default = action.dest.upper()
                args_string = self._format_args(action, default)
                for option_string in action.option_strings:
                    parts.append('%s %s' % (option_string, args_string))
            if sum(len(s) for s in parts) < self._width - (len(parts) - 1) * 2:
                return ', '.join(parts)
            else:
                return ',\n  '.join(parts)

This code overrides the default argparse.HelpFormatter method _format_action_invocation, and is identical to the default implementation except in the last four lines.

Default formatter behavior:

parser = argparse.ArgumentParser(description="Argparse default formatter.")
parser.add_argument('-a', '--argument', help='not too long')
parser.add_argument('-u', '--ugly', choices=range(20), help='looks messy')
parser.print_help()

outputs:

usage: test.py [-h] [-a ARGUMENT]
               [-u {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}]

Argparse default formatter.

optional arguments:
  -h, --help            show this help message and exit
  -a ARGUMENT, --argument ARGUMENT
                        not too long
  -u {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}, --ugly {0,1,2,3,4,5,6,
7,8,9,10,11,12,13,14,15,16,17,18,19}
                        looks messy

Custom formatter behavior:

parser = argparse.ArgumentParser(description="Argparse custom formatter.",
                                 formatter_class=CustomFormatter)
parser.add_argument('-a', '--argument', help='not too long')
parser.add_argument('-l', '--less-ugly', choices=range(20), help='less messy')

outputs:

usage: test.py [-h] [-a ARGUMENT]
               [-l {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}]

Argparse custom formatter.

optional arguments:
  -h, --help            show this help message and exit
  -a ARGUMENT, --argument ARGUMENT
                        not too long
  -l {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19},
  --less-ugly {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}
                        less messy

Solution 5 - Python

To get the expected output, you'll need to subclass argparse.HelpFormatter and implement the formatting that you need. In particular, you'll need to implement your own _metavar_formatter method, which is the one in charge of joining all the choices into a single string separated by commas.

Solution 6 - Python

Why not used parser.add_argument_group to create a group for your server based options and give that a description arg displays the list of possible choices? Then pass argparse.SUPPRESS into the help for each of the individual options. I believe that will give you what you want.

Solution 7 - Python

http://bugs.python.org/issue16468 argparse only supports iterable choices is the bugs issue discussing the formatting of choices. The list of choices can appear in 3 places: the usage line, the help lines, and error messages.

All that parser cares about is doing an in (__contains__) test. But for formatting, long lists, unbounded 'lists' (e.g. integers >100), and other objects that aren't iterable give problems. metavar is the way that current users can get around most formatting issues (it may not help with the error messages). Look at the issue to get ideas of how to change your own version of argparse.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDeletedView Question on Stackoverflow
Solution 1 - Pythonuser2463717View Answer on Stackoverflow
Solution 2 - PythonErnest AView Answer on Stackoverflow
Solution 3 - PythonMikeView Answer on Stackoverflow
Solution 4 - PythonvaminView Answer on Stackoverflow
Solution 5 - PythonjcolladoView Answer on Stackoverflow
Solution 6 - PythonmichaelfilmsView Answer on Stackoverflow
Solution 7 - PythonhpauljView Answer on Stackoverflow