使用Python模块argparse的单个选项的自定义值名称和多个属性

时间:2015-10-19 13:53:02

标签: python options argparse

我在此发布此问题作为Q& A,因为我没有在网上找到解决方案,而且可能是其他人,而不是我一直在想这个,如果我错过了一些,请随时更新改进分。

问题是

  1. 如何更改argparse模块设置的帮助消息中选项值显示的名称
  2. 如何让argparse将选项的值拆分为ArgumentParser.parse_args()方法返回的对象的多个属性
  3. argparse模块设置的默认帮助消息中,使用大写字母的目标属性名称显示可选参数所需的值。然而,这可能会产生不合需要的长篇概要和选项帮助。例如。考虑脚本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()
    

    请求帮助

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

    其中选项-b和-c的值是不必要的详细信息,因为大多数选项对最终用户没有用,无法知道输入值保存在哪个属性下。

    此外,默认情况下argparse仅允许将选项值保存到ArgumentParser.parse_args()方法返回的对象的单个属性。然而,有时希望能够使用复杂的选项值,例如,以逗号分隔的列表,并已分配给多个属性。可以肯定的是,选项值的解析可以在以后完成,但是在argparse框架内完成所有解析以在错误的用户指定选项值上获得一致的错误消息是很好的。

2 个答案:

答案 0 :(得分:2)

您可以使用其他名称和metavar控制参数调用行。

如果我定义:

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

我得到了这些帮助热线:

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

帮助中使用了第一个“long”选项标志。 metavar参数允许您直接指定该字符串。

Explanation for argparse python modul behaviour: Where do the capital placeholders come from?是此行中较早的问题,答案很短metavar

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

还有SO请求显示帮助,如:

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

这需要自定义HelpFormatter类。但是设置metavar=''可以帮助您实现目标:

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

请参阅python argparse help message, disable metavar for short options?

至于分割参数,可以在自定义Action类中完成。但我认为解析后这样做更简单。您仍然可以发出标准化的错误消息 - parse.error(...)来电。

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让你接受3个参数(选择你的号码)。 Namespace值将是一个列表,您可以轻松地将其分配给其他变量或属性。这样的nargs负责计算参数。输入必须是空格分隔的,就像其他参数一样。

如果您更喜欢使用逗号分隔列表,请注意逗号+空格分隔。您的用户可能必须在整个列表中放置引号。 https://stackoverflow.com/a/29926014/901925

答案 1 :(得分:0)

解决方案是使用ArgumentParserAction类的自定义版本。在ArgumentParser类中,我们覆盖parse_args()方法,以便能够将None值设置为未使用的多个属性(问题2)。在Action类中,我们为__init__方法添加了两个参数:

  • attr:逗号分隔的属性名称字符串,用于添加值,例如attr="a1,a2,a3"期望以逗号分隔的三个值列表存储在属性" a1"," a2"和" a3"之下。如果是attr 未使用且使用dest且包含逗号,这将取代attr的使用,例如dest="a1,a2,a3"等同于指定attr="a1,a2,a3"
  • action_type:将值转换为的类型,例如int,或用于转换的函数名称。这是必要的,因为在调用动作处理程序之前执行了类型转换,因此无法使用type参数。

下面的代码实现了这些自定义类,并在最后给出了一些调用示例:

#! /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'])