5

I'd like to build a parser.add_argument(...) to map given argument with constant defined in my code.

Suppose I have the following

import argparse

# Both are the same type
CONST_A = <something>
CONST_B = <otherthing>

parser = argparse.ArgumentParser()
parser.add_argument(...)

# I'd like the following to be true:
parser.parse_args("--foo A".split()).foo == CONST_A
parser.parse_args("--foo B".split()).foo == CONST_B

What can I put in lieu of ...?


The best I could do with const was:

import argparse

# Both are the same type
CONST_A = 10
CONST_B = 20

parser = argparse.ArgumentParser()
status_group = parser.add_mutually_exclusive_group(required=True)
status_group.add_argument("-a", const=CONST_A, action='store_const')
status_group.add_argument("-b", const=CONST_B, action='store_const')

# I'd like the following to be true:
print parser.parse_args("-a".split()).a == CONST_A # True
print parser.parse_args("-b".split()).b == CONST_B # True

Note that the constants are saved into tw odifferent attributes a and b, witch suits me not :(

YSC
  • 38,212
  • 9
  • 96
  • 149
  • have you executed `help(argparse)` or checked out the documentation? (https://docs.python.org/3/library/argparse.html) I am looking into it for the first time now. – Tadhg McDonald-Jensen Feb 26 '16 at 09:42
  • I have, but it's a bit hairy for a beginner. BTW, I'm writing python2 code. – YSC Feb 26 '16 at 09:46
  • There are `const` and `default` parameters to `add_argument`. Experiment with those. –  Feb 26 '16 at 09:49
  • @Evert the best I could do with `const` and `action="store_const"` was to map my constants into two different attribute of returned namespace. – YSC Feb 26 '16 at 09:57

5 Answers5

9

The simplest way is to take advantage of the type= option in add_argument like @hpaulj did although it can be generalized with a factory function:

def argconv(**convs):
    def parse_argument(arg):
        if arg in convs:
            return convs[arg]
        else:
            msg = "invalid choice: {!r} (choose from {})"
            choices = ", ".join(sorted(repr(choice) for choice in convs.keys()))
            raise argparse.ArgumentTypeError(msg.format(arg,choices))
    return parse_argument

then in lieu of ... just use type=argconv(A=CONST_A, B=CONST_B):

parser.add_argument("--foo", type=argconv(A=CONST_A, B=CONST_B))

And then everything will work as you want it to in your example.


The following is the first answer I posted, it is still valid but isn't nearly as simple as the above solution.

An alternate method is to make a class that inherits from argparse.ArgumentParser and override parse_args to modify the result as it is generated:

import argparse

class MappedParser(argparse.ArgumentParser):
    mapping = {} #backup if you don't use def_mapping

    def def_mapping(self,**options):
        self.mapping = options

    def parse_args(self,args=None,namespace=None):
        result = argparse.ArgumentParser.parse_args(self,args,namespace)
        for name,options in self.mapping.items(): #by default this is is empty so the loop is skipped
            if name in result:
                key = getattr(result,name)
                if key in options:
                    replace_with = options[key]
                    setattr(result,name,replace_with)
                else:
                    self.error("option {name!r} got invalid value: {key!r}\n must be one of {valid}".format(name=name,key=key,valid=tuple(options.keys())))
                    return #error should exit program but I'll leave this just to be safe.
        return result

this way the rest of your (example) program would look like this:

# There is nothing restricting their type.
CONST_A = "<something>"
CONST_B = ["other value", "type is irrelevent"]

parser = MappedParser() #constructor is same

parser.def_mapping(foo={"A":CONST_A, "B":CONST_B})

parser.add_argument("--foo") # and this is unchanged

# the following is now true:
print(parser.parse_args("--foo A".split()).foo is CONST_A)
print(parser.parse_args("--foo B".split()).foo is CONST_B)
#note that 'is' operator works so it is even the same reference

#this gives decent error message
parser.parse_args("--foo INVALID".split())

print("when parser.error() is called the program ends so this never is printed")

Add extra options like this:

parser.def_mapping(foo={"A":CONST_A, "B":CONST_B,"C":"third option"})

or extra arguments like this:

parser.def_mapping(foo={"A":CONST_A, "B":CONST_B},
                   conv={"int":int,"float":float})

as well any added arguments that are not specified in def_mapping are left alone so it is very easy to implement.

Community
  • 1
  • 1
Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59
4

This is an interesting question. To the best of my knowledge, argparse does not support this directly.

If you find this pattern occurs often, you can write a small utility class that does this for you, by transforming args into a dictionary via vars:

class Switcher(object):
    def __init__(self, d):
        self._d = d

    def __call__(self, args):
        args_ = vars(args)

        for k, v in self._d.items():
            if args_[k]:
                return v

You can use it as follows. Say your parser is defined by:

import argparse

parser = argparse.ArgumentParser()
g = parser.add_mutually_exclusive_group()
g.add_argument('-a', action='store_true', default=False)
g.add_argument('-b', action='store_true', default=False)

Then you can define a Switcher via:

s = Switcher({'a': 10, 'b': 20})

and use it like so:

>>> print s(parser.parse_args(['-a']))
10 

>>> print s(parser.parse_args(['-b']))
20
Community
  • 1
  • 1
Ami Tavory
  • 74,578
  • 11
  • 141
  • 185
3

This would be a good case for a custom type parameter:

CONST_A='<A>'
CONST_B='<B>'
def footype(astring):
    dd = {'A':CONST_A, 'B':CONST_B}
    try:
        return dd[astring]
    except KeyError:
        raise argparse.ArgumentTypeError('enter A or B')

parser = argparse.ArgumentParser()
parser.add_argument('--foo', type=footype)

It will produce a namespace like

Namespace(foo='<A>')

and error message (if given --foo C) like:

usage: stack35648071.py [-h] [--foo FOO]
stack35648071.py: error: argument --foo: enter A or B

I tried adding choices but the help message isn't right. Use metavar and help to instruct your users.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
2

I wouldn't create a new action or object: just use a dict to store the constants, limit the values of --foo with choices and use parsed_args.foo to index the dict:

import argparse

CONST = {'A': 'something',
         'B': 'otherthing'}

parser = argparse.ArgumentParser()
parser.add_argument('--foo', choices=('A', 'B'))

assert CONST[parser.parse_args("--foo A".split()).foo] == 'something'
assert CONST[parser.parse_args("--foo B".split()).foo] == 'otherthing'

The combination of choices and the keys to the dict essentially fixes your results of foo.


And if you like to make things a bit clearer or easier, you can do reassign foo to your dict value after you parse the arguments:
args = parser.parse_args("--foo A".split())
args.foo = CONST[args.foo]

and now args.foo equates directly to 'something'.

1

According to the docs:

The action keyword argument specifies how the command-line arguments should be handled. You may also specify an arbitrary action by passing an Action subclass or other object that implements the same interface.

In [5]: import argparse 
   ...:  
   ...: class MappingAction(argparse.Action): 
   ...:     def __init__(self, option_strings, dest, mapping, **kwargs): 
   ...:         self.mapping = mapping 
   ...:         super(MappingAction, self).__init__(option_strings, dest, **kwargs) 
   ...:  
   ...:     def __call__(self, parser, namespace, values, option_string=None): 
   ...:         values = self.mapping.get(values, None) 
   ...:         setattr(namespace, self.dest, values) 
   ...:                                                                                                                                                       

In [6]: parser = argparse.ArgumentParser()   
   ...: mapping = { 
   ...:     'A': '<something>', 
   ...:     'B': '<otherthing>', 
   ...: } 
   ...: parser.add_argument('bar', action=MappingAction, choices=mapping.keys(), mapping=mapping)                                                                                     

In [7]: args = parser.parse_args('A'.split())                                                                                                                 

In [8]: args.bar                                                                                                                                              
Out[8]: '<something>'
Danil
  • 4,781
  • 1
  • 35
  • 50