Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

argparse: map user input to defined constant

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

like image 688
YSC Avatar asked Dec 01 '22 16:12

YSC


2 Answers

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.

like image 85
Tadhg McDonald-Jensen Avatar answered Dec 04 '22 14:12

Tadhg McDonald-Jensen


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
like image 32
Ami Tavory Avatar answered Dec 04 '22 15:12

Ami Tavory