Issue with argparse invalid choice in Python - python

When running python test.py red,
test.py:
from argparse import ArgumentParser
from enum import Enum
Color = Enum("Color", ["red", "green," "blue"])
parser = ArgumentParser()
parser.add_argument("color", type=Color, choices=[c.name for c in Color])
args = parser.parse_args()
print(f"your color is {args.color}")
class test:
def __init__(self):
self.a = 1
self.b = 2
I keep getting the following error, even though red is one of the options
usage: test.py [-h] {red,green,blue}
test.py: error: argument color: invalid Color value: 'red'
Have tried so many different things.

The choices argument should be instances of Color to match the class specified by your type.
The type kwarg should be a function that translates from string to Color instance, which then must be one of your choices. Sometimes you can put the class itself, only if its __init__ method can be used to do the conversion. Otherwise you need to use a helper function or lambda to translate from string to instance.
Try this:
Color = Enum("Color", ["red", "green", "blue"])
parser = ArgumentParser()
parser.add_argument("color", type=lambda arg: Color[arg], choices=Color)
args = parser.parse_args()
print(f"your color is {args.color}")
$ python main.py red
your color is Color.red
Alternatively, you could keep the argument as a string, and then convert it to a color after you finish parsing.
parser = ArgumentParser()
parser.add_argument("color", choices=[c.name for c in Color])
args = parser.parse_args()
color = Color[args.color]

Related

Python ArgParse works even though script is called with argument with similar name

I have the following code:
import argparse
# Create the parser
parser = argparse.ArgumentParser("ArgParse intro")
# Add an argument
mand_arg_group = parser.add_argument_group("Mandatory arguments")
mand_arg_group.add_argument('--name', type = str, required = True, help = "This is the name of the person we are processing")
# Add more arguments
# If required is missing it is considered to be False
opt_arg_group = parser.add_argument_group("Optional arguments")
opt_arg_group.add_argument('--age', type = int, help = "This is the age of the person we are processing")
# nargs specifies how many arguments to expect. If we don't know how many values the user will input we can use nargs='+'
opt_arg_group.add_argument('--tasks', type = int, help = "These are the IDs of the tasks this person has completed ", nargs = '+')
# Mutually exclusive arguments
mut_ex_group = parser.add_mutually_exclusive_group()
mut_ex_group.add_argument('--day', type = str, help = "Day shift")
mut_ex_group.add_argument('--night', type = str, help = "Night shift")
# Parse the arguments
args = parser.parse_args()
if args.day:
shift = args.day
else:
shift = args.night
output_string = f"{args.name} has {args.age} years and completed tasks with the following IDs: {args.tasks}. {args.name} is working the {shift} shift"
print(output_string)
I have called the same script in two different ways and it still works:
First way: python argparse_intro.py --name NAME1 --tasks 90 90 90
Second way: python argparse_intro.py --name NAME1 --task 90 90 90
It seems that it doesn't matter if I spell task or tasks, the argument parser will recognize the value and variable and it prints out the correct message without throwing an error
parse_args accepts any unique prefix of a long option name:
The parse_args() method by default allows long options to be abbreviated to a prefix, if the abbreviation is unambiguous (the prefix matches a unique option):
If you had defined a second option like --taskserver, then --task would raise an error, because parse_args couldn't tell if you meant --tasks --taskserver. (--tasks itself would be unambiguous as the full name of an option, rather than an abbreviation of --taskserver.)
You can force the parser to ignore abbreviations with setting allow_abbrev to false.
parser = argparse.ArgumentParser("ArgParse intro", allow_abbrev=False)

Setting a function with a parser command

When I launch my code I would like to choose a function (from a set of functions) which will be used. Unfortunately I have many loops and the code is very expensive so the following pseudocode is is highly deprecated:
import argparse
import mystuff
code body in which my_variable gets created
if args.my_index == 1:
def my_func(my_variable):
return my_variable + 1
if args.my_index == 2:
def my_func(my_variable):
return my_variable**2 +1
having used the following command:
$ python3 my_code.py --index 1
I was thinking about promoting the function to an external class module, maybe using the properties of class initialization.
You can register your functions inside a container like a tuple. Then your can retrieve them by index. the .index attribute of your ArgumentParser object is going to be 1 more than the tuple indices:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--index', type=int)
args = parser.parse_args()
my_variable = 10
funcs = (lambda x: x + 1, lambda x: x ** 2 + 1, lambda x: x ** 3 + 1)
if args.index is not None:
print(funcs[args.index - 1](my_variable))
This way when you execute your script using python3 my_code.py --index 1, the .index is 1, so you need to get the first item of the tuple which is args.index - 1.
output: 11
If by any chance your functions follow a specific pattern(like my_variable ** n + 1 here) you can define a generic function that handles it without registering all the functions:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--index', type=int)
args = parser.parse_args()
my_variable = 10
def func(x):
return my_variable ** x + 1
if args.index is not None:
print(func(args.index))
Here's an example how you might use a base class with your functions as classes inheriting from it to automatically build your commandline (using the __subclasses__() method on the base class) and do the calculation, without writing some ridiculous sequence of if statements.
For more info on subclasses see here in the Python documentation.
import argparse
# base class for all your functions to inherit from
class mystuff():
pass
# function classes inherit from mystuff, provide at least a docstring and a calculate() method.
class fun1(mystuff):
'''
Calculate fun1
'''
def calculate(self,value):
return value*2
class fun2(mystuff):
'''
Calculate fun2
'''
def calculate(self,value):
return value*23
if __name__=="__main__":
parser = argparse.ArgumentParser(description="Allow calling functions each defined in their own class")
# find all the classes to be used as commandline arguments
for fun in mystuff.__subclasses__():
# print( f"Creating argument for {fun.__name__}" )
parser.add_argument( f'--{fun.__name__}', default=0, dest='function', action='store_const', const=fun, help=fun.__doc__)
parser.add_argument('variable', type=int, help="Value to pass to calculation")
args = parser.parse_args()
# create an instance and call its calculate function passing the variable value
result = args.function().calculate(args.variable)
print( f"Calling {args.function.__name__} with variable {args.variable} gives result {result}" )
Getting the usage:
fun1.py -h
gives:
usage: fun1.py [-h] [--fun1] [--fun2] variable
Allow calling functions each defined in their own class
positional arguments:
variable Value to pass to calculation
optional arguments:
-h, --help show this help message and exit
--fun1 Calculate fun1
--fun2 Calculate fun2
invoking one of the functions:
fun1.py --fun1 235
gives result:
Calling fun1 with variable 235 gives result 470
I'm sure this could be made more sophisticated, perhaps removing the need for a -- before each function name by using subparsers.

Best way to make command line utility out of python function or method?

I write lots of little utility functions which I would like to make available both directly through the command line but also importable as python functions to be used by other utilities. Currently what I do is write my function in a file, and in the same file under if __name__ == "__main__": I use argparse to interface with the function on the command line. For example, let's say I have the file math.py:
import argparse
def add_or_subtract(a: float, b: float, c: float = 1., add: bool = True) -> float:
"""
Do some random math
Parameters
----------
a : float
A number
b : float
Another number
c : float, optional
Another number
add : bool, optional
Whether to add or subtract c
Returns
-------
answer : float
The answer
"""
if add:
return a+b+c
else:
return a+b-c
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("a", type=float, help="a number")
parser.add_argument("b", type=float, help="another number")
parser.add_argument("-c", "--c", type=float, help="another number", default=1.)
parser.add_argument("-a", "--add", action=store_true)
parser.parse_args()
print(add_or_subtract(parser.a, parser.b, parser.c, parser.add))
Basically I have the feeling that I am doing a lot of duplication defining arguments, their acceptable types, and their explanations. If I change some arguments on the function I have to remember to update it in three places. I'm wondering if there is an easier way.
I've been playing a bit with inspect to add CLI arguments based on the arguments in the function, but I want something a bit "smarter" that knows the difference between mandatory and optional arguments, acceptable types, boolean flags etc. It would be even greater if the docstrings could also be parsed for the help. The ideal scenario would be a kind of decorator that "command-lineifies" the function.
Does something like I'm describing exist? Or are there better ways of doing what I want.
Why don't you try implementing commands as classes and later just inherit those?
Something like this might work:
class CommandArgument:
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def add_to_parser(self, parser: argparse.ArgumentParser):
parser.add_argument(*self.args, **self.kwargs)
class BaseAddOrSubCommand:
a = CommandArgument('a', type=float, help='a number')
b = CommandArgument('b', type=float, help='another number')
c = CommandArgument('b', type=float, help='another number')
args = [a, b, c]
def __init__(self):
self.parser = argparse.ArgumentParser
for arg in self.args:
arg.add_to_parser(self.parser)
self.parser.parse_args()
def execute(self):
if self.parser.add:
return self.parser.a + self.parser.b + self.parser.c
else:
return self.parser.a + self.parser.b - self.parser.c
class MultiplySumOrSubCommand(BaseAddOrSubCommand):
mult = CommandArgument('mult', type=float, help='multiply result with me')
args = BaseAddOrSubCommand.args + [mult]
def execute(self):
return super().execute() * self.parser.mult
if __name__ == '__main__':
command = MultiplySumOrSubCommand()
print(command.execute())

argparse action or type for comma-separated list

I want to create a command line flag that can be used as
./prog.py --myarg=abcd,e,fg
and inside the parser have this be turned into ['abcd', 'e', 'fg'] (a tuple would be fine too).
I have done this successfully using action and type, but I feel like one is likely an abuse of the system or missing corner cases, while the other is right. However, I don't know which is which.
With action:
import argparse
class SplitArgs(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values.split(','))
parser = argparse.ArgumentParser()
parser.add_argument('--myarg', action=SplitArgs)
args = parser.parse_args()
print(args.myarg)
Instead with type:
import argparse
def list_str(values):
return values.split(',')
parser = argparse.ArgumentParser()
parser.add_argument('--myarg', type=list_str)
args = parser.parse_args()
print(args.myarg)
The simplest solution is to consider your argument as a string and split.
#!/usr/bin/env python3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--myarg", type=str)
d = vars(parser.parse_args())
if "myarg" in d.keys():
d["myarg"] = [s.strip() for s in d["myarg"].split(",")]
print(d)
Result:
$ ./toto.py --myarg=abcd,e,fg
{'myarg': ['abcd', 'e', 'fg']}
$ ./toto.py --myarg="abcd, e, fg"
{'myarg': ['abcd', 'e', 'fg']}
I find your first solution to be the right one. The reason is that it allows you to better handle defaults:
names: List[str] = ['Jane', 'Dave', 'John']
parser = argparse.ArumentParser()
parser.add_argument('--names', default=names, action=SplitArgs)
args = parser.parse_args()
names = args.names
This doesn't work with list_str because the default would have to be a string.
Your custom action is the closest way to how it is done internally for other argument types. IMHO there should be a _StoreCommaSeperatedAction added to argparse in the stdlib since it is a somewhat common and useful argument type,
It can be used with an added default as well.
Here is an example without using an action (no SplitArgs class):
class Test:
def __init__(self):
self._names: List[str] = ["Jane", "Dave", "John"]
#property
def names(self):
return self._names
#names.setter
def names(self, value):
self._names = [name.strip() for name in value.split(",")]
test_object = Test()
parser = ArgumentParser()
parser.add_argument(
"-n",
"--names",
dest="names",
default=",".join(test_object.names), # Joining the default here is important.
help="a comma separated list of names as an argument",
)
print(test_object.names)
parser.parse_args(namespace=test_object)
print(test_object.names)
Here is another example using SplitArgs class inside a class completely
"""MyClass
Demonstrates how to split and use a comma separated argument in a class with defaults
"""
import sys
from typing import List
from argparse import ArgumentParser, Action
class SplitArgs(Action):
def __call__(self, parser, namespace, values, option_string=None):
# Be sure to strip, maybe they have spaces where they don't belong and wrapped the arg value in quotes
setattr(namespace, self.dest, [value.strip() for value in values.split(",")])
class MyClass:
def __init__(self):
self.names: List[str] = ["Jane", "Dave", "John"]
self.parser = ArgumentParser(description=__doc__)
self.parser.add_argument(
"-n",
"--names",
dest="names",
default=",".join(self.names), # Joining the default here is important.
action=SplitArgs,
help="a comma separated list of names as an argument",
)
self.parser.parse_args(namespace=self)
if __name__ == "__main__":
print(sys.argv)
my_class = MyClass()
print(my_class.names)
sys.argv = [sys.argv[0], "--names", "miigotu, sickchill,github"]
my_class = MyClass()
print(my_class.names)
And here is how to do it in a function based situation, with a default included
class SplitArgs(Action):
def __call__(self, parser, namespace, values, option_string=None):
# Be sure to strip, maybe they have spaces where they don't belong and wrapped the arg value in quotes
setattr(namespace, self.dest, [value.strip() for value in values.split(",")])
names: List[str] = ["Jane", "Dave", "John"]
parser = ArgumentParser(description=__doc__)
parser.add_argument(
"-n",
"--names",
dest="names",
default=",".join(names), # Joining the default here is important.
action=SplitArgs,
help="a comma separated list of names as an argument",
)
parser.parse_args()
I know this post is old but I recently found myself solving this exact problem. I used functools.partial for a lightweight solution:
import argparse
from functools import partial
csv_ = partial(str.split, sep=',')
p = argparse.ArgumentParser()
p.add_argument('--stuff', type=csv_)
p.parse_args(['--stuff', 'a,b,c'])
# Namespace(stuff=['a', 'b', 'c'])
If you're not familiar with functools.partial, it allows you to create a partially "frozen" function/method. In the above example, I created a new function (csv_) that is essentially a copy of str.split() except that the sep argument has been "frozen" to the comma character.

Python command line argument assign to a variable

I have to either store the command line argument in a variable or assign a default value to it.
What i am trying is the below
import sys
Var=sys.argv[1] or "somevalue"
I am getting the error out of index if i don't specify any argument. How to solve this?
Var=sys.argv[1] if len(sys.argv) > 1 else "somevalue"
The builtin argparse module is intended for exactly these sorts of tasks:
import argparse
# Set up argument parser
ap = argparse.ArgumentParser()
# Single positional argument, nargs makes it optional
ap.add_argument("thingy", nargs='?', default="blah")
# Do parsing
a = ap.parse_args()
# Use argument
print a.thingy
Or, if you are stuck with Python 2.6 or earlier, and don't wish to add a requirement on the backported argparse module, you can do similar things manually like so:
import optparse
opter = optparse.OptionParser()
# opter.add_option("-v", "--verbose") etc
opts, args = opter.parse_args()
if len(args) == 0:
var = "somevalue"
elif len(args) == 1:
var = args[0]
else:
opter.error("Only one argument expected, got %d" % len(args))
print var
Good question.
I think the best solution would be to do
try:
var = sys.argv[1]
except IndexError:
var = "somevalue"
Try the following with a command-line-processing template:
def process_command_line(argv):
...
# add your option here
parser.add_option('--var',
default="somevalue",
help="your help text")
def main(argv=None):
settings, args = process_command_line(argv)
...
print settings, args # <- print your settings and args
Running ./your_script.py with the template below and your modifications above prints {'var': 'somevalue'} []
For an example of a command-line-processing template see an example in Code Like a Pythonista: Idiomatic Python (http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#command-line-processing):
#!/usr/bin/env python
"""
Module docstring.
"""
import sys
import optparse
def process_command_line(argv):
"""
Return a 2-tuple: (settings object, args list).
`argv` is a list of arguments, or `None` for ``sys.argv[1:]``.
"""
if argv is None:
argv = sys.argv[1:]
# initialize the parser object:
parser = optparse.OptionParser(
formatter=optparse.TitledHelpFormatter(width=78),
add_help_option=None)
# define options here:
parser.add_option( # customized description; put --help last
'-h', '--help', action='help',
help='Show this help message and exit.')
settings, args = parser.parse_args(argv)
# check number of arguments, verify values, etc.:
if args:
parser.error('program takes no command-line arguments; '
'"%s" ignored.' % (args,))
# further process settings & args if necessary
return settings, args
def main(argv=None):
settings, args = process_command_line(argv)
# application code here, like:
# run(settings, args)
return 0 # success
if __name__ == '__main__':
status = main()
sys.exit(status)

Categories