argparse with multiple flags and arguments - python-3.x

I want my code to be able to call different functions according to the flag and then use the argument passed after the flag as the input to the function.
Example of the expected output:
$ python3 test.py -a 2
4
$ python3 test.py -a human123
<Error message>
$ python3 test.py -h human123
Hello, human123
Here's my sample code:
class Test:
def __init__(self):
pass
def add(self, a):
return a+a
def hello(self, name):
return f"Hello, {name}"
parser = argparse.ArgumentParser()
parser.add_argument('-a', '--add', dest='command', action='store_consts', const='add', nargs=1, help='add a to itself')
parser.add_argument('-h', '--hello', dest='command', action='store_consts', const='hello', nargs=1, help='hello!')
args = parser.parse_args()
t = Test()
if args.command=='add':
print(t.add(args.add))
elif args.command=='sub':
print(t.hello(args.hello))
This sample code currently is doing what I want to achieve. I tried many things to fix the issue from removing the 'consts', changing the action to 'store', changing the value of nargs to '?', etc., however, it keeps giving me different kinds of errors like TypeError, etc.

Simplifying your arguments:
import argparse
class Test:
def __init__(self):
pass
def add(self, a):
return a+a
def hello(self, name):
return f"Hello, {name}"
parser = argparse.ArgumentParser()
parser.add_argument('-a', '--add', help='add a to itself')
parser.add_argument('-b', '--hello', help='hello!') # -h is already taken
args = parser.parse_args()
print(args)
t = Test()
if args.add: # or 'args.add is not None:'
print(t.add(args.add))
elif args.hello:
print(t.hello(args.hello))
test runs:
1936:~/mypy$ python3 stack62702524.py -a testing
Namespace(add='testing', hello=None)
testingtesting
1937:~/mypy$ python3 stack62702524.py -b other
Namespace(add=None, hello='other')
Hello, other
1937:~/mypy$ python3 stack62702524.py
Namespace(add=None, hello=None)
===
Your errors, which you did not show :{ When you get errors, don't just throw up your hands and randomly try alternatives. Read the docs and try to understand the error.
parser.add_argument('-c', action='store_consts', const='hello')
ValueError: unknown action "store_consts"
parser.add_argument('-c', action='store_const', const='hello', nargs=1)
TypeError: __init__() got an unexpected keyword argument 'nargs'
'store_consts' is an name error; with 'store_const' nargs is fixed at 0; you can't change that.
If I add 3 arguments - two store_const and one positional:
parser.add_argument('-c', dest='command', action='store_const', const='add')
parser.add_argument('-d', dest='command', action='store_const', const='hello')
parser.add_argument('foo')
Note the two new command and foo attributes, which you could use in your function call:
1945:~/mypy$ python3 stack62702524.py -c bar
Namespace(add=None, command='add', foo='bar', hello=None)
1945:~/mypy$ python3 stack62702524.py -d bar
Namespace(add=None, command='hello', foo='bar', hello=None)

Typically I use the dest parameter of add_argument to specify the variable name.
For example:
parser.add_argument("-a", "--add", dest="add", required=False, type=str help="some help")
Could be accessed by:
args = parser.parse_args()
print(args.add == "something")
I believe that you need one unique dest per argument.
Also, -h is reserved for help. You may wish to change this to -e or something.
parser.add_argument('-h', '--hello', ...)

Related

Argparse Unit Test with IP Address Module

I'd like to write unit tests for the following function:
#!/usr/bin/env python3
"""IPv4 validation using `ipaddress module` and argparse."""
import argparse
from ipaddress import ip_address
def parse_cli_args():
"""
Command line parser for subnet of interest.
Args:
--ip 0.0.0.0
Returns:
String, e.g. 0.0.0.0
"""
parser = argparse.ArgumentParser(description="IPv4 address of interest.")
parser.add_argument("--ip", action="store", type=ip_address,\
required=True,\
help="IP address of interest, e.g. 0.0.0.0")
args = parser.parse_args()
return args
if __name__ == '__main__':
args = parse_cli_args()
print(args.ip)
which works as expected, e.g.:
python3 test.py --ip 192.168.1.1
192.168.1.1
python3 test.py --ip derp
usage: test.py [-h] --ip IP
test.py: error: argument --ip: invalid ip_address value: 'derp'
python3 test.py --ip
usage: test.py [-h] --ip IP
test.py: error: argument --ip: expected one argument
How can I mock these three conditions in unit tests?
I tried a few variations of this:
import unittest
from unittest.mock import patch
class ParseCLIArgs(unittest.TestCase):
"""Unit tests."""
#patch('builtins.input', return_value='192.168.1.1')
def test_parse_cli_args_01(self, input):
"""Valid return value."""
self.assertIsInstance(parse_cli_args(), ipaddress.IPv4Address)
if __name__ == '__main__':
unittest.main()
without success. What am I doing wrong, and how can I fix that?
EDIT I got a bit further with this:
class ParseCLIArgs(unittest.TestCase):
def setUp(self):
self.parser = parse_cli_args()
def test_parser_cli_args(self):
parsed = self.parser.parse_args(['--ip', '192.168.1.1'])
self.assertIs(parsed.ip, '192.168.1.1')
if __name__ == '__main__':
unittest.main()
Which fails with: TypeError: isinstance() arg 2 must be a type or tuple of types. I believe this is because the function actually transforms user input.
To test a parser you need to either modify sys.argv or provide your own substitute.
When called with
args = parser.parse_args(argv)
if argv is None (or not provided) it parses sys.argv[1:]. This is the list that the shell/interpreter gives it. Otherwise it will parse the equivalent list that you provide.
test_argparse.py uses both ways to test a parser - constructing a custom sys.argv and call parse_args with a custom argv.
Another thing to watch out for is error trapping. Most parsing errors display usage, error message and then exits. Capturing that exit and the stderr message takes some work. test_argparse makes a subclass of ArgumentParser with a custom error method.
In sum, doing unittest on code that depends on sys.argv and does a system exit, may require more work than it's worth. That said, I'm not an expert on unit testing and mocking tools; I've just studied the test_argparse file.
Hopefully this can serve as good sample for someone with a similar question. This test does most (but not all) of what I want it to do. I'm still working out a unit test to check for Type.
#!/usr/bin/env python3
"""IPv4 validation using `ipaddress module` and argparse."""
import argparse
from ipaddress import ip_address
import unittest
def parse_cli_args():
"""
Command line parser for subnet of interest.
Args:
--ip 0.0.0.0
Returns:
String, e.g. 0.0.0.0
"""
parser = argparse.ArgumentParser(description="IPv4 address of interest.")
parser.add_argument("--ip", action="store",\
required=True,\
help="IP address of interest, e.g. 0.0.0.0")
return parser
class ParseCLIArgs(unittest.TestCase):
def setUp(self):
self.parser = parse_cli_args()
def test_parser_cli_args(self):
parsed = self.parser.parse_args(['--ip', '192.168.1.1'])
self.assertEqual(parsed.ip, '192.168.1.1')
if __name__ == '__main__':
unittest.main()

pytest: passing list as arguments from command line is not working

I am facing below error when i run pytest with list as arguments from command line..
pytest -vs test_sample.py --html=results.html --A_list=[A1, A2, A3]
ERROR: file not found: A2,
below is my test_sample.py code
import pytest
def test_functionality(A_list):
print("element in list: {}".format(A_list))
Below is my conftest.py code
def pytest_addoption(parser):
parser.addoption("--A_list", action="store", default="default name")
def pytest_generate_tests(metafunc):
option_value = metafunc.config.option.A_list
if 'A_list' in metafunc.fixturenames and option_value is not None:
metafunc.parametrize("A_list", [option_value])
This is working fine if i pass only one element like below
pytest -vs test_sample.py --html=results.html --A_list=A1
But it is getting failed if i run pytest with list of elements for A_list like below
pytest -vs test_sample.py --html=results.html --A_list=[A1, A2, A3]
Can anyone let me know how to pass list as pytest arguments from command line ...
The problem is that you try to pass a Python list as a command line argument. This does not work - command line arguments are just strings that need to be parsed. Especially you cannot have spaces in the option if you don't surround it with apostrophes.
What you could do is to pass the list as a string, for example by comma separating the entries:
pytest -vs test_sample.py --html=results.html --A_list="A1,A2,A3"
Note that the apostrophes are strictly not needed here as you don't have spaces, but you can use them anyway. Then you can parse the string into a list:
def pytest_generate_tests(metafunc):
option_value = metafunc.config.option.A_list
if option_value:
params = option_value.split(",")
if 'A_list' in metafunc.fixturenames:
metafunc.parametrize("A_list", params)

In argparse, how can I make positional arguments depend on the optional arguments?

How can I make positional arguments depend on the optional arguments?
For example:
parser.add_argument("-A", action="store_true", help = "add")
parser.add_argument("-U", action="store_true", help = "update")
parser.add_argument("-D", action="store_true", help = "delete")
parser.add_argument("-L", action="store_true", help = "list")
If I choose -A , I want it to require arguments "name , address, cp number"
But if I choose -L, I don't want it to require anything or when I choose -U it requires another set of arguments.
My end goal is to create a contact book where I can add new contacts, update existing contacts, delete contacts and list contacts. I can do this if I use if else statements but I want to try using argparse.
If I'm using argparse incorrectly, please give me some advice!
This is a good opportunity to use subparsers:
This flips (I think) what you're trying to acheive.
The help text looks like this:
😊 /tmp python3 test.py --help
usage: PROG [-h] {add,update,delete,list} ...
positional arguments:
{add,update,delete,list}
Sub-commands
add ADD help text
update UPDATE help text
delete DELETE help text
list LIST help text
optional arguments:
-h, --help show this help message and exit
See '<command> --help' to read about a specific sub-command.
😊 /tmp python3 test.py add -h
usage: PROG add [-h] [--foo FOO]
optional arguments:
-h, --help show this help message and exit
--foo FOO
This skeleton should get you started:
import argparse
def run_command(parser, args):
if args.command == 'add':
print(args)
elif args.command == 'update':
print(args)
elif args.command == 'delete':
print(args)
elif args.command == 'list':
print(args)
parser = argparse.ArgumentParser(
prog='PROG',
epilog="See '<command> --help' to read about a specific sub-command."
)
subparsers = parser.add_subparsers(dest='command', help='Sub-commands')
add_parser = subparsers.add_parser('add', help='ADD help text')
add_parser.add_argument("--foo")
add_parser.set_defaults(func=run_command)
update_parser = subparsers.add_parser('update', help='UPDATE help text')
update_parser.add_argument('--bar')
update_parser.set_defaults(func=run_command)
delete_parser = subparsers.add_parser('delete', help='DELETE help text')
delete_parser.add_argument('--baz')
delete_parser.set_defaults(func=run_command)
list_parser = subparsers.add_parser('list', help='LIST help text')
list_parser.add_argument('--qux')
list_parser.set_defaults(func=run_command)
args = parser.parse_args()
if args.command is not None:
args.func(parser, args)
else:
parser.print_help()
What the code above does is:
Creates an argparse.ArgumentParser
Adds a subparser called command
Then we create 4 parsers (one for each action) that we can add arguments to
Finally we set a func to run_command which we pass the parser and the args.

how to pass command line argument from pytest to code

I am trying to pass arguments from a pytest testcase to a module being tested. For example, using the main.py from Python boilerplate, I can run it from the command line as:
$ python3 main.py
usage: main.py [-h] [-f] [-n NAME] [-v] [--version] arg
main.py: error: the following arguments are required: arg
$ python3 main.py xx
hello world
Namespace(arg='xx', flag=False, name=None, verbose=0)
Now I am trying to do the same with pytest, with the following test_sample.py
(NOTE: the main.py requires command line arguments. But these arguments need to be hardcoded in a specific test, they should not be command line arguments to pytest. The pytest testcase only needs to send these values as command line arguments to main.main().)
import main
def test_case01():
main.main()
# I dont know how to pass 'xx' to main.py,
# so for now I just have one test with no arguments
and running the test as:
pytest -vs test_sample.py
This fails with error messages. I tried to look at other answers for a solution but could not use them. For example, 42778124 suggests to create a separate file run.py which is not a desirable thing to do. And 48359957 and 40880259 seem to deal more with command line arguments for pytest, instead of passing command line arguments to the main code.
I dont need the pytest to take command line arguments, the arguments can be hardcoded inside a specific test. But these arguments need to be passed as arguments to the main code. Can you give me a test_sample.py, that calls main.main() with some arguments?
If you can't modify the signature of the main method, you can use the monkeypatching technique to temporarily replace the arguments with the test data. Example: imagine writing tests for the following program:
import argparse
def main():
parser = argparse.ArgumentParser(description='Greeter')
parser.add_argument('name')
args = parser.parse_args()
return f'hello {args.name}'
if __name__ == '__main__':
print(main())
When running it from the command line:
$ python greeter.py world
hello world
To test the main function with some custom data, monkeypatch sys.argv:
import sys
import greeter
def test_greeter(monkeypatch):
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['greeter', 'spam'])
assert greeter.main() == 'hello spam'
When combined with the parametrizing technique, this allows to easily test different arguments without modifying the test function:
import sys
import pytest
import greeter
#pytest.mark.parametrize('name', ['spam', 'eggs', 'bacon'])
def test_greeter(monkeypatch, name):
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['greeter', name])
assert greeter.main() == 'hello ' + name
Now you get three tests, one for each of the arguments:
$ pytest -v test_greeter.py
...
test_greeter.py::test_greeter[spam] PASSED
test_greeter.py::test_greeter[eggs] PASSED
test_greeter.py::test_greeter[bacon] PASSED
A good practice might to have this kind of code, instead of reading arguments from main method.
# main.py
def main(arg1):
return arg1
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='My awesome script')
parser.add_argument('word', help='a word')
args = parser.parse_args()
main(args.word)
This way, your main method can easily be tested in pytest
import main
def test_case01():
main.main(your_hardcoded_arg)
I am not sure you can call a python script to test except by using os module, which might be not a good practice

Python3 Argparse metavar brackets parsed weirdly

I am using argparse in python3, and I get some strange things:
A short version of code that I'm using is:
argparser = argparse.ArgumentParser(description='add/remove items')
argparser.add_argument('-a', action='append', metavar="Item(s)", help='add one or more items to the list')
argparser.add_argument('-r', action='append', metavar="Item(s)", help='remove one or more items from the list')
args = argparser.parse_args()
When I run the script with the -h flag, I get this output:
usage: test.py [-h] [-a Items)] [-r Item(s]
add/remove items
optional arguments:
-h, --help show this help message and exit
-a CPE(s) add one or more items to the list
-r CPE(s) remove one or more items from the list
Mind the weird parsing of the brackets in the first line.
What causes this, and how do I solve this?
It's the () in your metavars that is causing the mangled usage. The usage formatter uses () to mark require mutually exclusive groups, and then removes surplus ones. So it tries to keep ( -o | -t), but change (-o) to -o. Unfortunately that code does not distinguish between the ones it added and the ones you added via the metavar (or help line).
Your line was formatted as:
usage: test.py [-h] [-a Item(s)] [-r Item(s)]
but it removed the outer pair of () which I have replaced with *:
usage: test.py [-h] [-a Item*s)] [-r Item(s*]
http://bugs.python.org/issue11874 focuses on a different usage problem, one which occurs when the usage line is long and needs to be split. But the last 2 posts in that issue deal with this issue.
If you don't like the limitations of the automatic usage formatting, you can give the parser your own custom usage parameter.
Since you want to have the possibility of having multiple items. Another way to do this with argparse is as follows:
import argparse
argparser = argparse.ArgumentParser(description='add/remove items')
argparser.add_argument('-a', metavar="item", nargs="*", help='add one or more items to the list')
argparser.add_argument('-r', metavar="item", nargs="*", help='remove one or more items from the list')
args = argparser.parse_args()
The key point is the use of nargs="*" (0 or more arguments). The help becomes:
usage: test.py [-h] [-a [item [item ...]]] [-r [item [item ...]]]
This way, you do not have to use "Item(s)", and you also follow a standard practice.
PS: I see what you wanted to do. With action="append", you are actually allowing the user to specify multiple -a and -r options. In this case, you should definitely write "Item" (and not "Item(s)"), since each option takes a single item. This solves your problem too (your help message should indicate that multiple -a and -r options can be given).
My solution in Python 2.7 was to override argparse.format_usage() and argparse.format_help(). You encode your metavars and then decode them after argparse does its formatting:
FIXES = (('\[', '%lb'), ('\]', '%rb'), ('\(', '%lp'), ('\)', '%rp'))
def encode_parens(text):
for orig, encoded in FIXES:
text = re.sub(orig, encoded, text)
return text
def decode_parens(text):
for orig, encoded in FIXES:
text = re.sub(encoded, orig[1:], text)
return text
class MyArgParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
super(MyArgParser, self).__init__(*args, **kwargs)
def format_usage(self, *args, **kwargs):
u = super(MyArgParser, self).format_usage(*args, **kwargs)
return decode_parens(u)
def format_help(self, *args, **kwargs):
h = super(MyArgParser, self).format_help(*args, **kwargs)
return decode_parens(h)
if __name__ == '__main__':
argparser = MyArgParser(description='add/remove items')
argparser.add_argument('-a', action='append', metavar=encode_parens("Item(s)"), help='add one or more items to the list')
argparser.add_argument('-r', action='append', metavar=encode_parens("Item(s)"), help='remove one or more items from the list')
args = argparser.parse_args()
That yields what you want:
usage: arg.py [-h] [-a Item(s)] [-r Item(s)]
It also fixes square brackets, which argparse doesn't like either.

Resources