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'])