Make functions importable when using argparse - python

I have the following CLI program which adds two numbers:
import argparse
def foo(args):
print('X + Y:', args.x + args.y)
return args.x + args.y
if __name__ == '__main__':
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
foo_parser = subparsers.add_parser('foo')
foo_parser.add_argument('-x', type=int, default=1)
foo_parser.add_argument('-y', type=int, default=2)
foo_parser.set_defaults(func=foo)
parser.add_argument('--debug', action='store_true', default=False)
args = parser.parse_args()
args.func(args)
Suppose now I want my users to also be able to import foo and call it directly with arguments x and y. I.e. I want foo to look like this:
def foo(x, y):
print('X + Y:', x + y)
return x + y
How can I adapt args.func(args) to handle this new foo?

The use of args.func(**vars(args)) is not a best fit for use-cases which require import as well as a CLI view
When users import a function and call it, they expect a return value for further processing (nothing printed on a console. The caller can decide to print based on the result obtained)
When they use a CLI, they expect to see an output printed on the console and a proper exit code (0 or 1 based on the return value)
The ideal way is to separate the parsing/CLI management/sum-function into separate functions and delegate processing once the parsing is complete (below is a sample example)
from __future__ import print_function # for python2
import argparse
def mysum(x, y=5):
return x+y
def delegator(input_args):
func_map = {
"mysum" : {
"func" : mysum,
"args": (input_args.get("x"),),
"kwargs" : {
"y" : input_args.get("y")
},
"result_renderer": print
}
}
func_data = func_map.get(input_args.get("action_to_perform"))
func = func_data.get("func")
args = func_data.get("args")
kwargs = func_data.get("kwargs")
renderer = func_data.get("result_renderer")
renderer(func(*args, **kwargs))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="action_to_perform")
foo_parser = subparsers.add_parser('mysum')
foo_parser.add_argument('-x', metavar='PATH', type=int, default=1)
foo_parser.add_argument('-y', metavar='PATH', type=int, default=3)
delegator(vars(parser.parse_args()))
Above example would also remove *args, **kwargs from your original function and lets the delegator send only what is needed by the function based on the command
You can extend it to support multiple commands
Hope this helps!

This is the cleanest way I've found so far:
import argparse
def foo(*, x, y, **kwargs):
print('X + Y:', x + y)
return x + y
if __name__ == '__main__':
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
foo_parser = subparsers.add_parser('foo')
foo_parser.add_argument('-x', metavar='PATH', type=int, default=1)
foo_parser.add_argument('-y', metavar='PATH', type=int, default=2)
foo_parser.set_defaults(func=foo)
parser.add_argument('--debug', action='store_true', default=False)
args = parser.parse_args()
args.func(**vars(args))
Maybe someone else can find something better.

Related

How to unit test a python script with nested modules accepting command line arguments?

I have an example code like this. How to unit test gen_a, gen_b, gen_c,...,gen_z with different input a,b,c (through command line)? I know, my codes are terribly organized.... These modules do not take any input. So, how to set command line arguments in unit test? Thanks!
import argparse
def parse_args():
parser = argparse.ArgumentParser(description='Davidson')
parser.add_argument('-a', '--a', type=int, default=1,)
parser.add_argument('-b', '--b', type=int, default=2)
parser.add_argument('-c', '--c', type=int, default=3)
return parser.parse_args()
def gen_d():
args = parse_args()
a = args.a
b = args.b
c = args.c
'''using a,b,c to create d '''
return d
def gen_e():
d = gen_d()
'''using d create e '''
return e
def gen_f():
e = gen_e()
'''using e create f '''
return f
def gen_g():
f = gen_f()
'''using f create g '''
return g
'''and so on'''
def gen_z():
y = gen_y()
'''using y create z '''
return z
The command-line is represented by a list variable in Python by the sys.argv variable which you can explictly set in each unit test with the arguments you want to specify.
The parse_args() function by default uses sys.argv as the list of arguments but you can override this by specifying an explicit list of arguments; e.g. parser.parse_args(args=myargs).
== mylib.py ==
import argparse
def parse_args(s):
parser = argparse.ArgumentParser(description='Davidson')
parser.add_argument('-a', '--a', type=int, default=1)
parser.add_argument('-b', '--b', type=int, default=2)
parser.add_argument('-c', '--c', type=int, default=3)
return parser.parse_args()
def gen_d():
args = parse_args()
a = args.a
b = args.b
c = args.c
# using a,b,c to create d
d = a + b + c
return d
'''and so on'''
Here is sample unittest class calling your code.
== mytest.py ==
import unittest
import sys
import mylib
class MyTest(unittest.TestCase):
def test_gen_d(self):
# mock command-line args to your script:
sys.argv = ['python', '--a=100', '--b=20']
value = mylib.gen_d()
self.assertEqual(123, value)
if __name__ == '__main__':
unittest.main()

Python argparse default values not working

I'm experimenting with argparse, the program works, but default values don't work. Here's my code:
'''This simple program helps you in understanding how to feed the user input from command line and to show help on passing invalid argument.'''
import argparse
import sys
def calc(args):
#Enable variables within the function to take on values from 'args' object.
operation = args.operation
x = args.x
y = args.y
if (operation == "add"):
return x + y
elif (operation == "sub"):
return x - y
parser = argparse.ArgumentParser(description="This is a summing program") #parser is an object of the class Argument Parser.
parser.add_argument("x", type=float, default=1.0, help="What is the first number?") #add_argument is a method of the class ArgumentParser.
parser.add_argument("y", type=float, default=1.0, help='What is the second number?')
parser.add_argument("operation", type=str, default="add", help='What operation? Can choose add, sub, mul, or div')
args = parser.parse_args()
print(str(calc(args)))
This simple program work, however attempting to call it without values returns the following error:
usage: cmdline.py [-h] x y operation
cmdline.py: error: the following arguments are required: x, y, operation
Where am I going wrong?
You are missing nargs='?'. The following works:
import argparse
import sys
def calc(args):
#Enable variables within the function to take on values from 'args' object.
operation = args.operation
x = args.x
y = args.y
if (operation == "add"):
return x + y
elif (operation == "sub"):
return x - y
parser = argparse.ArgumentParser(description="This is a summing program") #parser is an object of the class Argument Parser.
parser.add_argument("x", nargs='?', type=float, default=1.0, help="What is the first number?") #add_argument is a method of the class ArgumentParser.
parser.add_argument("y", nargs='?', type=float, default=1.0, help='What is the second number?')
parser.add_argument("operation", nargs='?', type=str, default="add", help='What operation? Can choose add, sub, mul, or div')
args = parser.parse_args()
print(str(calc(args)))
Change these lines to indicate that you want named, optional command-line arguments (so "-x" not "x"):
parser.add_argument("-x", type=float, default=1.0, help="What is the first number?") #add_argument is a method of the class ArgumentParser.
parser.add_argument("-y", type=float, default=1.0, help='What is the second number?')
#jbcoe - I think you have a few typoes in your code, but thank you, it works! Here's the solution, cleaned up:
'''This simple program helps you in understanding how to feed the user input from command line and to show help on passing invalid argument. It uses the argparse module.'''
import argparse
def calc(args):
#Enable variables within the function to take on values from 'args' object.
operation = args.operation
x = args.x
y = args.y
if (operation == "add"):
return x + y
elif (operation == "sub"):
return x - y
parser = argparse.ArgumentParser(description="This is a summing program") #parser is an object of the class Argument Parser.
parser.add_argument("x", nargs='?', type=float, default=1.0, help="What is the first number?") #add_argument is a method of the class ArgumentParser.
parser.add_argument("y", nargs='?', type=float, default=2.0, help='What is the second number?')
parser.add_argument("operation", nargs='?', type=str, default="add", help='What operation? Can choose add, sub, mul, or div')
args = parser.parse_args()
print(str(calc(args)))

how to apply options for the functions using argparse

I have a functions like this
def add(x,y):
print x+y
def square(a):
print a**2
Now I am defining linux commands(options) for this functions using argparse.
I tried with this code
import argparse
# Create Parser and Subparser
parser = argparse.ArgumentParser(description="Example ArgumentParser")
subparser = parser.add_subparsers(help="commands")
# Make Subparsers
add_parser = subparser.add_parser('--add', help="add func")
add_parser.add_argument("x",type=float,help='first number')
add_parser.add_argument("y",type=float,help='second number')
add_parser.set_defaults(func='add')
square_parser = subparser.add_parser('--square', help="square func")
square_parser.add_argument("a",type=float,help='number to square')
square_parser.set_defaults(func='square')
args = parser.parse_args()
def add(x,y):
print x + y
def square(a):
print a**2
if args.func == '--add':
add(args.x,args.y)
if args.func == '--square':
square(args.a)
But I am getting error while passing command as python code.py --add 2 3
invalid choice: '2' (choose from '--add', '--square')
--add is the form of an optionals flag, add is the correct form for a subparser name
import argparse
# Create Parser and Subparser
parser = argparse.ArgumentParser(description="Example ArgumentParser")
subparser = parser.add_subparsers(dest='cmd', help="commands")
# Make Subparsers
add_parser = subparser.add_parser('add', help="add func")
add_parser.add_argument("x",type=float,help='first number')
add_parser.add_argument("y",type=float,help='second number')
add_parser.set_defaults(func='add')
square_parser = subparser.add_parser('square', help="square func")
square_parser.add_argument("a",type=float,help='number to square')
square_parser.set_defaults(func='square')
args = parser.parse_args()
print(args)
def add(x,y):
print x + y
def square(a):
print a**2
if args.func == 'add': # if args.cmd=='add': also works
add(args.x,args.y)
if args.func == 'square':
square(args.a)
producing
0950:~/mypy$ python stack43557510.py add 2 3
Namespace(cmd='add', func='add', x=2.0, y=3.0)
5.0
I added dest='cmd' to the add_subparsers command, and print(args) to give more information. Note that the subparser name is now available as args.cmd. So you don't need the added func.
However the argparse docs do suggest an alternative use of set_defaults
https://docs.python.org/3/library/argparse.html#sub-commands
add_parser.set_defaults(func=add)
With this args.func is actually a function object, not just a string name. So it can be used as
args.func(args)
Note that I had to change how the functions handle their parameters:
def add(args):
print(args.x + args.y)
def square(args):
print(args.a**2)
# Create Parser and Subparser
parser = argparse.ArgumentParser(description="Example ArgumentParser")
subparser = parser.add_subparsers(dest='cmd', help="commands")
# Make Subparsers
add_parser = subparser.add_parser('add', help="add func")
add_parser.add_argument("x",type=float,help='first number')
add_parser.add_argument("y",type=float,help='second number')
add_parser.set_defaults(func=add)
square_parser = subparser.add_parser('square', help="square func")
square_parser.add_argument("a",type=float,help='number to square')
square_parser.set_defaults(func=square)
args = parser.parse_args()
print(args)
args.func(args)
producing
1001:~/mypy$ python stack43557510.py add 2 3
Namespace(cmd='add', func=<function add at 0xb73fd224>, x=2.0, y=3.0)
5.0

argparse in separate function inside a class and calling args from init

I have a question regarding passing args to variables inside init
Here are my working version of code.
class A:
def __init__(self):
self.id = args.id
self.pw = args.pw
self.endpoint = args.endpoint
def B:
..do something..
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--id', type=str, help = 'username')
parser.add_argument('-p', '--pw', type=str, help ='password')
parser.add_argument('-e', '--end_point', type=str , help='end point of api')
args = parser.parse_args()
The above code works, but what I am trying right now is to put all the argparse codes inside a function inside class A and assign arg to init.
I looked in the web and I couldn't find a good solution.
class A:
def __init__(self):
self.id = self.parse_args(id)
self.pw = self.parse_args(pw)
self.endpoint = self.parse_args(endpoint)
def B:
..do something..
def parse_args(self,args):
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--id', type=str, help = 'username')
parser.add_argument('-p', '--pw', type=str, help ='password')
parser.add_argument('-e', '--end_point', type=str , help='end point of api')
return parser.parse_args(args)
Ok, so to be exact, I'm not sure how I should approach this problem.
In above example, I just called args.variable for args to work but in this case I call self.id = self.parse_args.id?
parse_args function return args and I also tried self.id = self.parse_args(id) and this is giving me
TypeError: 'builtin_function_or_method' object is not iterable
The part of reason why I want to separate args into a separate function is to simplify my unit test with argparse.
So in the first case, you must be doing
if __name__ ...
....
args = parser.parse_args()
a = A()
The A.__init__ can see args because it is global.
I'm don't see why you'd want to make the argparse code part of A; you don't want it to run every time you use A() do you? You could only make one set of values.
I think it would be test to make the parse_args code a method, that can be run, at will, after creating the class.
Here's an approach that has, I think, pretty good flexibility:
import argparse
class A:
def __init__(self, id=None, pw=None, endpoint=None):
self.id = id
self.pw = pw
self.endpoint = endpoint
def parse_args(self, argv=None):
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--id', type=str, help = 'username')
parser.add_argument('-p', '--pw', type=str, help ='password')
parser.add_argument('-e', '--end_point', type=str , help='end point of api')
args = parser.parse_args(argv)
self.id = args.id
self.pw = args.pw
self.endpoint = args.end_point
def __str__(self):
return 'A(%s,...)'%self.id
if __name__ == "__main__":
a = A()
print(a)
a.parse_args()
print(a)
b = A(id='you')
print(b)
b.parse_args(['--id','me'])
print(b)
Values can be set during object creation, from commandline or from custom argv
1610:~/mypy$ python stack39967787.py --id joe
A(None,...)
A(joe,...)
A(you,...)
A(me,...)
==================
My 1st method (temporarily deleted)
class A():
def __init__(self):
args = self.parse_args()
self.a = args.a
etc
#static_method
def parse_args(self):
parser = ....
return parser.parse_args()
=====================
Your class A could be used as Namespace, letting parse_args update the artributes directly (it uses setattr.
import argparse
class A:
def __init__(self, id=None, pw=None, endpoint=None):
self.id = id
self.pw = pw
self.endpoint = endpoint
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--id', type=str, help = 'username')
parser.add_argument('-p', '--pw', type=str, help ='password')
parser.add_argument('-e', '--endpoint', type=str , help='end point of api')
args = parser.parse_args()
print(args)
a = A()
parser.parse_args(namespace=a)
print(vars(a))
producing:
1719:~/mypy$ python stack39967787_1.py --id joe --pw=xxxx -e1
Namespace(endpoint='1', id='joe', pw='xxxx')
{'endpoint': '1', 'id': 'joe', 'pw': 'xxxx'}

Is there a pythonic way of assigning values to variables when passed in from the command line?

I have written to following code but it feels very clunky and I was wondering if there was a pythonic way of writing the following code:
import argparse
foo = 0
bar = 1
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--foo", type=int,
help="foo")
parser.add_argument("-b", "--bar", type=int,
help="bar")
args = parser.parse_args()
if args.foo: # This is the bit that I think is clunky
foo = args.foo #
if args.bar: #
bar = args.bar #
In my code I have about 7 different arguments and having a list of if statements doesn't seem like the best method. Is there a better way of writing this section?
argparse have default arguments, so there is no need for the ifs. Also, you should seperate argument parsing and processing, therefore you don't need local variables for your args, but can use them as parameters. So you would finally get to something like this:
import argparse
def some_function(foo, bar):
pass
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--foo", type=int, default=0,
help="foo")
parser.add_argument("-b", "--bar", type=int, default=1,
help="bar")
args = parser.parse_args()
some_function(args.foo, args.bar)
if __name__ == '__main__':
main()
In argsparse there are default and required fields that can help you reduce the amount of if's you have.
Example:
parser = argparse.ArgumentParser()
parser.add_argument("-a", "--foo", type=int, required=True,
help="foo")
parser.add_argument("-b", "--bar", type=int, default=42,
help="bar")
You can also return the args object, and accessing the args at a later point.

Categories

Resources