1

I'm posting this here as Q&A since I did not find a solution available on the web and potentially others than I have been wondering about this, please feel free to update improve if I have missed out on some points.

The questions are

  1. How does one change what name is displayed for the option value in the help message set up by the argparse module
  2. How can one get argparse to split the value of an option into several attributes of the object returned by the ArgumentParser.parse_args() method

In the default help message set up by the argparse module the value needed by an optional argument is displayed using the name of the destination attribute in capital letters. This can however give rise to undesirable long synopsis and option help. E.g. consider the script a.py:

#! /usr/bin/env python
import sys
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument('-a')
parser.add_argument('-b','--b_option')
parser.add_argument('-c','--c_option',dest='some_integer')
args = parser.parse_args()

Calling the help of this yields

>>> a.py -h
usage: SAPprob.py [-h] [-a A] [-b B_OPTION] [-c SOME_INTEGER]

optional arguments:
  -h, --help            show this help message and exit
  -a A
  -b B_OPTION, --b_option B_OPTION
  -c SOME_INTEGER, --c_option SOME_INTEGER
>>> 

where the value of options -b and -c are unnecessarily detailed as it most of is of no use to the end user to know which attribute the input value gets saved under.

Further, by default argparse only allows saving option values to a single attribute of the object returned by the ArgumentParser.parse_args() method. However, sometimes it is desirable to be able to use a complex option value, e.g. a comma separated list, and to have assigned to multiple attributes. For sure, the parsing of the option value can be done later but it would be neat to have all the parsing done within the argparse framework to get a consistent error message upon erroneous user specified option values.

ShadowRanger
  • 124,179
  • 11
  • 158
  • 228
cpaitor
  • 335
  • 1
  • 2
  • 15

2 Answers2

2

You can control the argument invocation line with other names and with metavar.

If I define:

parser.add_argument('-f','--foo','--foo_integer',help='foo help')
parser.add_argument('-m','--m_string',metavar='moo',help='foo help')

I get these help lines:

  -f FOO, --foo FOO, --foo_integer FOO
                        foo help
  -m moo, --m_string moo
                        foo help

The first 'long' option flag is used in the help. The metavar parameter lets you specify that string directly.

Explanation for argparse python modul behaviour: Where do the capital placeholders come from? is an earlier question along this line, with a short metavar answer.

and

How do I avoid the capital placeholders in python's argparse module?

There have also been SO requests to display help like:

  -f,--foo, --foo_integer FOO  foo help

This requires a customization of the HelpFormatter class. But setting metavar='' gets you part way there:

  -f,--foo, --foo_integer  foo help (add metavar info to help)

See python argparse help message, disable metavar for short options?

As for splitting an argument, that could be done in a custom Action class. But I think it is simpler to do it after parsing. You can still issue standardized error messages - with a parse.error(...) call.

In [14]: parser.error('this is a custom error message')
usage: ipython3 [-h] [-a A] [-b B_OPTION] [-c SOME_INTEGER] [-f FOO] [-m moo]
ipython3: error: this is a custom error message
...

nargs=3 lets you accept exactly 3 arguments (pick your number). The Namespace value will be a list, which you can easily assign to other variables or attributes. An nargs like this takes care of counting the arguments. The inputs must be space separated, just like other arguments.

If you prefer to use a comma separated list, be wary of comma+space separation. Your user might have to put quotes around the whole list. https://stackoverflow.com/a/29926014/901925

Community
  • 1
  • 1
hpaulj
  • 201,845
  • 13
  • 203
  • 313
  • seems like a classical case of rtfm, I feel a bit ashamed that I totally missed out on `metavar`. I do agree that for a simple argument value to be assigned to an attribute `metavar` is the preferred way to go about, whereas parsing multiple values into multiple attributes I still preferr custom `Action` and `ArgumentParser` classes. Thanks to your answer I've also updated the code above to be able to handle the `nargs` argument – cpaitor Oct 20 '15 at 10:26
0

The solution is to use custom versions of the ArgumentParser and the Action class. In the ArgumentParser class we override the parse_args() method to be able setting None value to unused multiple attributes (question 2). In the Action class we add two arguments to the __init__ method:

  • attr: comma separated string of attribute names to add values to, e.g. attr="a1,a2,a3" will expect a comma separated list of three values to be stored under attributes "a1", "a2", and "a3". If attr is not used and dest is used and contains commas this will replace the use of attr, e.g. dest="a1,a2,a3" will be equivalent to specifying attr="a1,a2,a3"
  • action_type: the type to convert values into, e.g. int, or name of function to use for conversion. This will be neccesary as the type conversion is performed prior to invoking the action handler, hence the type argument can not be used.

The code below implements these custom classes and gives some examples on their invocation at the end:

#! /usr/bin/env python
import sys
from argparse import ArgumentParser,Action,ArgumentError,ArgumentTypeError,Namespace,SUPPRESS
from gettext import gettext as _

class CustomArgumentParser(ArgumentParser):
   """   
   custom version of ArgumentParser class that overrides parse_args() method to assign
   None values to not set multiple attributes
   """
   def __init__(self,**kwargs):
      super(CustomArgumentParser,self).__init__(**kwargs)
   def parse_args(self, args=None, namespace=None):
      """ custom argument parser that handles CustomAction handler """
      def init_val_attr(action,namespace):
         ### init custom attributes to default value
         if hasattr(action,'custom_action_attributes'):
            na = len(action.custom_action_attributes)
            for i in range(na):
               val = None
               if action.default is not SUPPRESS and action.default[i] is not None:
                  val = action.default[i]
               setattr(namespace,action.custom_action_attributes[i],val)
      def del_tmp_attr(action,args):
         ### remove attributes that were only temporarly used for help pages
         if hasattr(action,'del_action_attributes'):
            delattr(args,getattr(action,'del_action_attributes'))

      if namespace is None:
         namespace = Namespace()

      ### Check for multiple attributes and initiate to None if present
      for action in self._actions:
         init_val_attr(action,namespace)
         ### Check if there are subparsers around
         if hasattr(action,'_name_parser_map') and isinstance(action._name_parser_map,dict):
            for key in action._name_parser_map.keys():
               for subaction in action._name_parser_map[key]._actions:
                  init_val_attr(subaction,namespace)

      ### parse argument list
      args, argv = self.parse_known_args(args, namespace)
      if argv:
         msg = _('unrecognized arguments: %s')
         self.error(msg % ' '.join(argv))

      ### remove temporary attributes
      for action in self._actions:
         del_tmp_attr(action,namespace)
         ### Check if there are subparsers around
         if hasattr(action,'_name_parser_map') and isinstance(action._name_parser_map,dict):
            for key in action._name_parser_map.keys():
               for subaction in action._name_parser_map[key]._actions:
                  del_tmp_attr(subaction,namespace)
      return args


class CustomAction(Action):
   """   
   Custom version of Action class that adds two new keyword argument to class to allow setting values
   of multiple attribute from a single option:
   :type  attr: string
   :param attr: Either list of/tuple of/comma separated string of attributes to assign values to,
                 e.g. attr="a1,a2,a3" will expect a three-element comma separated string as value 
                 to be split by the commas and stored under attributes a1, a2, and a3. If nargs 
                 argument is set values should instead be separated by commas and if nargs is set
                 to an integer value this must be equal or greater than number of attributes, or 
                 if args is set to "*" o "+" the number of values must atleast equal to the number 
                 of arguments. If nars is set and number of values are greater than the number of 
                 attributes the last attribute will be a list of the remainng values. If attr is 
                 not used argument dest will have the same functionality.
   :type  action_type: single type or function or list/tuple of
   :param action_type: single/list of/tuple of type(s) to convert values into, e.g. int, or name(s) of 
                        function(s) to use for conversion. If size of list/tuple of default parameters
                        is shorter than length of attr, list will be padded with last value in input list/ 
                        tuple to proper size

   Further the syntax of a keyword argument have been extended:
   :type  default: any compatible with argument action_type
   :param default: either a single value or a list/tuple of of values compatible with input argument
                     action_type. If size of list/tuple of default parameters is shorter than list of
                     attributes list will be padded with last value in input list/tuple to proper size
   """
   def __init__(self, option_strings, dest, nargs=None, **kwargs):
      def set_list_arg(self,kwargs,arg,types,default):
         if arg in kwargs:
            if not isinstance(kwargs[arg],list):
               if isinstance(kwargs[arg],tuple):
                  attr = []
                  for i in range(len(kwargs[arg])):
                     if types is not None:
                        attr.append(types[i](kwargs[arg][i]))
                     else:
                        attr.append(kwargs[arg][i])
                  setattr(self,arg,attr)
               else:
                  setattr(self,arg,[kwargs[arg]])
            else:
               setattr(self,arg,kwargs[arg])
            del(kwargs[arg])
         else:
            setattr(self,arg,default)

      ### Check for and handle additional keyword arguments, then remove them from kwargs if present
      if 'attr' in kwargs:
         if isinstance(kwargs['attr'],list) or isinstance(kwargs['attr'],tuple):
            attributes = kwargs['attr']
         else:
            attributes = kwargs['attr'].split(',')
         self.attr = attributes
         del(kwargs['attr'])
      else:
         attributes = dest.split(',')
      na = len(attributes)
      set_list_arg(self,kwargs,'action_type',None,[str])
      self.action_type.extend([self.action_type[-1] for i in range(na-len(self.action_type))])
      super(CustomAction, self).__init__(option_strings, dest, nargs=nargs,**kwargs)
      set_list_arg(self,kwargs,'default',self.action_type,None)

      # check for campatibility of nargs
      if isinstance(nargs,int) and nargs < na:
         raise ArgumentError(self,"nargs is less than number of attributes (%d)" % (na))

      ### save info on multiple attributes to use and mark destination as atribute not to use
      if dest != attributes[0]:
         self.del_action_attributes = dest
      self.custom_action_attributes = attributes

      ### make sure there are as many defaults as attributes
      if self.default is None:
         self.default = [None]
      self.default.extend([self.default[-1] for i in range(na-len(self.default))])

   def __call__(self, parser, namespace, values, options):
      ### Check if to assign to multiple attributes
      multi_val = True
      if hasattr(self,'attr'):
         attributes = self.attr
      elif ',' in self.dest:
         attributes = self.dest.split(',')
      else:
         attributes = [self.dest]
         multi_val = False
      na = len(attributes)
      if self.nargs is not None:
         values = values
      elif na > 1:
         values = values.split(',')
      else:
         values = [values]
      try:
         nv = len(values)
         if na > nv:
            raise Exception
         for i in range(na-1):
            setattr(namespace,attributes[i],self.action_type[i](values[i]))
         vals = []
         for i in range(na-1,nv):
            vals.append(self.action_type[-1](values[i]))
         setattr(namespace,attributes[-1],vals)
      except:
         if na > 1:
            if self.nargs is not None:
               types = ' '.join([str(self.action_type[i])[1:-1] for i in range(na)])
               if multi_val:
                  raise ArgumentError(self,"value of %s option must be blank separated list of minimum %d items of: %s[ %s ...]" % (options,na,types,str(self.action_type[-1])[1:-1]))
               else:
                  raise ArgumentError(self,"value of %s option must be blank separated list of %d items of: %s" % (options,na,types))
            else:
               types = ', '.join([str(self.action_type[i])[1:-1] for i in range(na)])
               raise ArgumentError(self,"value of %s option must be tuple or list or comma separated string of %d items of: %s" % (options,na,types))
         else:
            raise ArgumentError(self,"failed to parse value of option %s" % (options))

### Some example invocations
parser = CustomArgumentParser()
parser.add_argument('-a',dest='n',action=CustomAction,type=int)
parser.add_argument('-b','--b_option',dest='m1,m2,m3',action=CustomAction,attr='b1,b2,b3',action_type=int)
parser.add_argument('-c','--c_option',dest='c1,c2,c3',action=CustomAction)
parser.add_argument('-d','--d_option',dest='d1,d2,d3',action=CustomAction,default=("1","2"))
parser.add_argument('-e','--e_option',dest='n,o,p',action=CustomAction,attr=('e1','e2','e3'),action_type=(int,str),default=("1","2"))
parser.add_argument('-f','--f_option',dest='f1,f2,f3',metavar="b,g,h",action=CustomAction,default=("1","2"),nargs=4)
print parser.parse_args(['-f','a','b','c','d'])
cpaitor
  • 335
  • 1
  • 2
  • 15
  • `parser.set_defaults` could be used to initial `m1`, `m2`, etc. Or you could define your own `Namespace`. That way you don't need to customize the parser, just the Action. – hpaulj Oct 19 '15 at 16:34