Python unittest and argparse with required value not given - python

I am having trouble unit-testing a function I made using argparse library. I would like to test the case when no argument is given. This is the function I would like to test:
def parse_arguments():
parser = argparse.ArgumentParser(description='Testing parsing')
parser.add_argument('--reqVal', required=True, help='Required value')
parser.add_argument('--notReq', required=False, help='Value not required')
return parser.parse_args()
Here are my tests:
import argparse
from parse_gen import parse_arguments
from unittest.mock import patch
def test_parse_arguments_only_required_args(capsys):
args_namespace = argparse.Namespace()
args_namespace.reqVal = 'Required'
args_namespace.notReq = None
with patch('sys.argv', ['parse_arguments', '--reqVal', 'Required']):
parsed_args = parse_arguments()
assert parsed_args == args_namespace
def test_parse_arguments_no_args(capsys):
with patch('sys.argv', ['parse_arguments']):
parsed_args = parse_arguments()
assert parsed_args == argparse.ArgumentTypeError()
First test case is working as expected. My problem is that when I want to test no_args case I get an error but it is very long and I do not understand how to fix it. Could you please help me?

Related

Make functions importable when using argparse

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.

How to execute python class modules along with pytest and option parser?

main
|--> src
|--> custom_calculation.py
|--> test_custom_calculation.py
custom_calculation.py
def calc_total(a,b):
return a+b
def calc_multiply(a,b):
return a*b
test_custom_calculation.py
import custom_calculation
def test_calc_sum():
total = custom_calculation.calc_total(10,10)
assert total == 20
def test_calc_multiply():
result = custom_calculation.calc_multiply(10,10)
assert result == 100
This is how i execute for simple modules.
cd main/src
python -m pytest
py.test -v
Learning python object oriented. Please help me if my code is wrong (could be even in importing module as well). Actual question here is, can i execute python (containing class) modules along with pytest and option parser ?
main
|--> A
|--> custom_calculation.py
|--> src
|--> test_custom_calculation.py
test_custom_calculation.py
from optparse import OptionParser
from A import *
import sys
class Test_Custom_Calculation():
def test_calc_sum():
total = custom_calculation.calc_total(10,10)
assert total == 20
def test_calc_multiply():
result = custom_calculation.calc_multiply(10,10)
assert result == 100
if __name__ == "__main__":
O = Test_Custom_Calculation()
parser = OptionParser()
parser.add_option("-a", "--a", dest="a", default=None,
help="Enter value of a")
parser.add_option("-b", "--b", dest="b", default=None,
help="Enter value of b")
parser.add_option("-o", "--o", dest="o", default=None,
help="specify operation to be performed")
(options, args) = parser.parse_args()
if options.a is None or options.b is None or options.c is None:
sys.exit("provide values of a,b and specify operation")
if options.c == "add":
O.test_calc_sum(a,b)
elif options.c == "mul":
O.test_calc_multiply(a,b)
else:
sys.exit("Specify appropriate operation")
without pytest, i can run this as python test_custom_calculation.py --a 10 --b 10 --c add
how can i run this with pytest ?
EDITED :
test_sample.py
def test_fun1(val1, val2, val3):
def test_fun2(val4,val5,val1):
def test_fun3(val6,val7,val8):
def test_fun4(val9,val10,val2):
def test_fun5(val2,val11,val10):
conftest.py
import pytest
def pytest_addoption(parser):
parser.add_option("-a", "--add", dest="add", default=None,
help="specifies the addition operation")
parser.add_option("-s", "--sub", dest="sub", default=None,
help="specifies the subtraction")
parser.add_option("-m", "--mul", dest="mul", default=None,
help="specifies the multiplication")
parser.add_option("-d", "--div", dest="div", default=None,
help="specifies the division")
parser.add_option("-t", "--trigonometry", dest="trigonometry", default=None,
help="specifies the trigonometry operation")
where to define those functional arguments val* ?
where can we decide the logic of handling optional parser ?
say, if option.add and option.sub:
sys.exit("Please provide only one option")
if option.add is None :
sys.exit("No value provided")
if option.add == "add":
test_fun1(val1,val2,val3)
According to your question, i understood that you want to pass operations(add,sub) as a command line parameters, and execute the operations with the various val*.
So in Pytest,
You can refer my answer:-- A way to add test specific params to each test using pytest
So it is based on test method name, logic should be handled in the fixture.
Yes, Pytest has inbuilt parser option for the testcase.
Defined the below method in conftest.py.
def pytest_addoption(parser):
"""
Command line options for the pytest tests in this module.
:param parser: Parser used for method.
:return: None
"""
parser.addoption("--o",
default=None,
actions="store"
help="specify operation to be performed")
Kindly refer https://docs.pytest.org/en/latest/example/simple.html for more detail.
Use command:--
pytest -vsx test_custom_calculations.py --a= --o=
In test method,
test_custom_calculations.py
def test_cal(request):
value_retrieved = request.config.getoption("--a")

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'}

Conditional passing of arguments to methods in python

I have many possible arguments from argparse that I want to pass to a function. If the variable hasn't been set, I want the method to use its default variable. However, handling which arguments have been set and which haven't is tedious:
import argparse
def my_func(a = 1, b = 2):
return a+b
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get the numeric values.')
parser.add_argument('-a', type=int)
parser.add_argument('-b', type=int)
args = parser.parse_args()
if not args.a is None and not args.b is None:
result = my_func(a = args.a, b = args.b)
elif not args.a is None and args.b is None:
result = my_func(a = args.a)
elif not args.b is None and args.a is None:
result = my_func(b = args.b)
else:
result = my_func()
It seems like I should be able to do something like this:
result = my_func(a = args.a if not args.a is None, b = args.b if not args.b is None)
But this gives a syntax error on the comma.
I could set default values in the argparser, but I want to use the defaults set in the method definition.
Use a dictionary with the kwargs unpacking syntax.
args = parser.parse_args()
result = my_func(**vars(args))
Edit
Use the SUPPRESS argument to ArgumentParser to remove empty values:
parser = argparse.ArgumentParser(description='Get the numeric values.',
argument_default=argparse.SUPPRESS)
The first solution that comes to me seems kind of hacky...but here it is.
Use inspect to write a function that looks at the arguments of a function and only passes it those arguments from args which it accepts and are not None. My guess is that this would be widely considered bad practice...
import inspect
def call_function(fn, args):
argspec = inspect.getargspec(fn)
arglist = {}
for arg in argspec.args:
if arg in args.__dict__.keys() and args.__dict__[arg] is not None:
arglist[arg] = args.__dict__[arg]
return fn(**arglist)
Here it is in action:
import argparse
def my_func(a=1, c=2):
return a,c
a=None
b=2
c=3
args=argparse.Namespace(a=a,b=b,c=c)
call_function(my_func, args)
>> (1, 3)
This solution is quite under-tested and might need work to make it more robust, but the idea is there and should work in simple cases.

Default sub-command, or handling no sub-command with argparse

How can I have a default sub-command, or handle the case where no sub-command is given using argparse?
import argparse
a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
a.parse_args()
Here I'd like a command to be selected, or the arguments to be handled based only on the next highest level of parser (in this case the top-level parser).
joiner#X:~/src> python3 default_subcommand.py
usage: default_subcommand.py [-h] {hi} ...
default_subcommand.py: error: too few arguments
On Python 3.2 (and 2.7) you will get that error, but not on 3.3 and 3.4 (no response). Therefore on 3.3/3.4 you could test for parsed_args to be an empty Namespace.
A more general solution is to add a method set_default_subparser() (taken from the ruamel.std.argparse package) and call that method just before parse_args():
import argparse
import sys
def set_default_subparser(self, name, args=None, positional_args=0):
"""default subparser selection. Call after setup, just before parse_args()
name: is the name of the subparser to call by default
args: if set is the argument list handed to parse_args()
, tested with 2.7, 3.2, 3.3, 3.4
it works with 2.6 assuming argparse is installed
"""
subparser_found = False
for arg in sys.argv[1:]:
if arg in ['-h', '--help']: # global help if no subparser
break
else:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in sys.argv[1:]:
subparser_found = True
if not subparser_found:
# insert default in last position before global positional
# arguments, this implies no global options are specified after
# first positional argument
if args is None:
sys.argv.insert(len(sys.argv) - positional_args, name)
else:
args.insert(len(args) - positional_args, name)
argparse.ArgumentParser.set_default_subparser = set_default_subparser
def do_hi():
print('inside hi')
a = argparse.ArgumentParser()
b = a.add_subparsers()
sp = b.add_parser('hi')
sp.set_defaults(func=do_hi)
a.set_default_subparser('hi')
parsed_args = a.parse_args()
if hasattr(parsed_args, 'func'):
parsed_args.func()
This will work with 2.6 (if argparse is installed from PyPI), 2.7, 3.2, 3.3, 3.4. And allows you to do both
python3 default_subcommand.py
and
python3 default_subcommand.py hi
with the same effect.
Allowing to chose a new subparser for default, instead of one of the existing ones.
The first version of the code allows setting one of the previously-defined subparsers as a default one. The following modification allows adding a new default subparser, which could then be used to specifically process the case when no subparser was selected by user (different lines marked in the code)
def set_default_subparser(self, name, args=None, positional_args=0):
"""default subparser selection. Call after setup, just before parse_args()
name: is the name of the subparser to call by default
args: if set is the argument list handed to parse_args()
, tested with 2.7, 3.2, 3.3, 3.4
it works with 2.6 assuming argparse is installed
"""
subparser_found = False
existing_default = False # check if default parser previously defined
for arg in sys.argv[1:]:
if arg in ['-h', '--help']: # global help if no subparser
break
else:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in sys.argv[1:]:
subparser_found = True
if sp_name == name: # check existance of default parser
existing_default = True
if not subparser_found:
# If the default subparser is not among the existing ones,
# create a new parser.
# As this is called just before 'parse_args', the default
# parser created here will not pollute the help output.
if not existing_default:
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
x.add_parser(name)
break # this works OK, but should I check further?
# insert default in last position before global positional
# arguments, this implies no global options are specified after
# first positional argument
if args is None:
sys.argv.insert(len(sys.argv) - positional_args, name)
else:
args.insert(len(args) - positional_args, name)
argparse.ArgumentParser.set_default_subparser = set_default_subparser
a = argparse.ArgumentParser()
b = a.add_subparsers(dest ='cmd')
sp = b.add_parser('hi')
sp2 = b.add_parser('hai')
a.set_default_subparser('hey')
parsed_args = a.parse_args()
print(parsed_args)
The "default" option will still not show up in the help:
python test_parser.py -h
usage: test_parser.py [-h] {hi,hai} ...
positional arguments:
{hi,hai}
optional arguments:
-h, --help show this help message and exit
However, it is now possible to differentiate between and separately handle calling one of the provided subparsers, and calling the default subparser when no argument was provided:
$ python test_parser.py hi
Namespace(cmd='hi')
$ python test_parser.py
Namespace(cmd='hey')
It seems I've stumbled on the solution eventually myself.
If the command is optional, then this makes the command an option. In my original parser configuration, I had a package command that could take a range of possible steps, or it would perform all steps if none was given. This makes the step a choice:
parser = argparse.ArgumentParser()
command_parser = subparsers.add_parser('command')
command_parser.add_argument('--step', choices=['prepare', 'configure', 'compile', 'stage', 'package'])
...other command parsers
parsed_args = parser.parse_args()
if parsed_args.step is None:
do all the steps...
Here's a nicer way of adding a set_default_subparser method:
class DefaultSubcommandArgParse(argparse.ArgumentParser):
__default_subparser = None
def set_default_subparser(self, name):
self.__default_subparser = name
def _parse_known_args(self, arg_strings, *args, **kwargs):
in_args = set(arg_strings)
d_sp = self.__default_subparser
if d_sp is not None and not {'-h', '--help'}.intersection(in_args):
for x in self._subparsers._actions:
subparser_found = (
isinstance(x, argparse._SubParsersAction) and
in_args.intersection(x._name_parser_map.keys())
)
if subparser_found:
break
else:
# insert default in first position, this implies no
# global options without a sub_parsers specified
arg_strings = [d_sp] + arg_strings
return super(DefaultSubcommandArgParse, self)._parse_known_args(
arg_strings, *args, **kwargs
)
Maybe what you're looking for is the dest argument of add_subparsers:
(Warning: works in Python 3.4, but not in 2.7)
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='cmd')
parser_hi = subparsers.add_parser('hi')
parser.parse_args([]) # Namespace(cmd=None)
Now you can just use the value of cmd:
if cmd in [None, 'hi']:
print('command "hi"')
You can duplicate the default action of a specific subparser on the main parser, effectively making it the default.
import argparse
p = argparse.ArgumentParser()
sp = p.add_subparsers()
a = sp.add_parser('a')
a.set_defaults(func=do_a)
b = sp.add_parser('b')
b.set_defaults(func=do_b)
p.set_defaults(func=do_b)
args = p.parse_args()
if args.func:
args.func()
else:
parser.print_help()
Does not work with add_subparsers(required=True), which is why the if args.func is down there.
In my case I found it easiest to explicitly provide the subcommand name to parse_args() when argv was empty.
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='commands')
runParser = subparsers.add_parser('run', help='[DEFAULT ACTION]')
altParser = subparsers.add_parser('alt', help='Alternate command')
altParser.add_argument('alt_val', type=str, help='Value required for alt command.')
# Here's my shortcut: If `argv` only contains the script name,
# manually inject our "default" command.
args = parser.parse_args(['run'] if len(sys.argv) == 1 else None)
print args
Example runs:
$ ./test.py
Namespace()
$ ./test.py alt blah
Namespace(alt_val='blah')
$ ./test.py blah
usage: test.py [-h] {run,alt} ...
test.py: error: invalid choice: 'blah' (choose from 'run', 'alt')
In python 2.7, you can override the error behaviour in a subclass (a shame there isn't a nicer way to differentiate the error):
import argparse
class ExceptionArgParser(argparse.ArgumentParser):
def error(self, message):
if "invalid choice" in message:
# throw exception (of your choice) to catch
raise RuntimeError(message)
else:
# restore normal behaviour
super(ExceptionArgParser, self).error(message)
parser = ExceptionArgParser()
subparsers = parser.add_subparsers(title='Modes', dest='mode')
default_parser = subparsers.add_parser('default')
default_parser.add_argument('a', nargs="+")
other_parser = subparsers.add_parser('other')
other_parser.add_argument('b', nargs="+")
try:
args = parser.parse_args()
except RuntimeError:
args = default_parser.parse_args()
# force the mode into namespace
setattr(args, 'mode', 'default')
print args
Here's another solution using a helper function to build a list of known subcommands:
import argparse
def parse_args(argv):
parser = argparse.ArgumentParser()
commands = []
subparsers = parser.add_subparsers(dest='command')
def add_command(name, *args, **kwargs):
commands.append(name)
return subparsers.add_parser(name, *args, **kwargs)
hi = add_command("hi")
hi.add_argument('--name')
add_command("hola")
# check for default command
if not argv or argv[0] not in commands:
argv.insert(0, "hi")
return parser.parse_args(argv)
assert parse_args([]).command == 'hi'
assert parse_args(['hi']).command == 'hi'
assert parse_args(['hi', '--name', 'John']).command == 'hi'
assert parse_args(['hi', '--name', 'John']).name == 'John'
assert parse_args(['--name', 'John']).command == 'hi'
assert parse_args(['hola']).command == 'hola'
You can add an argument with a default value that will be used when nothing is set I believe.
See this: http://docs.python.org/dev/library/argparse.html#default
Edit:
Sorry, I read your question a bit fast.
I do not think you would have a direct way of doing what you want via argparse. But you could check the length of sys.argv and if its length is 1 (only script name) then you could manually pass the default parameters for parsing, doing something like this:
import argparse
a = argparse.ArgumentParser()
b = a.add_subparsers()
b.add_parser('hi')
if len(sys.argv) == 1:
a.parse_args(['hi'])
else:
a.parse_args()
I think that should do what you want, but I agree it would be nice to have this out of the box.
For later reference:
...
b = a.add_subparsers(dest='cmd')
b.set_defaults(cmd='hey') # <-- this makes hey as default
b.add_parser('hi')
so, these two will be same:
python main.py hey
python main.py

Categories

Resources