Related
I have a small CLI app (myscript.py) that is defined like so.
import sys
import argparse
class MyParser(argparse.ArgumentParser):
'''
Overriden to show help on default.
'''
def error(self, message):
print(f'error: {message}')
self.print_help()
sys.exit(2)
def myfunc(args):
'''
Some function.
'''
print(args.input_int**2)
def main():
# Define Main Parser
main_par = MyParser(
prog='myapp',
description='main help')
# Define Command Parser
cmd_par = main_par.add_subparsers(
dest='command',
required=True)
# Add Subcommand Parser
subcmd_par = cmd_par.add_parser(
'subcmd',
description='subcmd help')
# Add Subcommand Argument
subcmd_par.add_argument(
'-i', '--input-int',
type=int,
help='some integer',
required=True)
# Add FromName Dispatcher
subcmd_par.set_defaults(
func=myfunc)
# Parse Arguments
args = main_par.parse_args()
# Call Method
args.func(args)
if __name__ == '__main__':
main()
The MyParser class simply overrides the error() method in argparse.ArgumentParser class to print help on error.
When I execute
$ python myscript.py
I see the default / main help. Expected.
When I execute
$ python myscript.py subcmd
I see the subcmd help. Expected.
When I execute
$ python myscript.py subcmd -i ClearlyWrongValue
I also see the subcmd help. Expected.
However, very annoyingly if I do the following
$ python myscript.py subcmd -i 2 --non-existent-argument WhateverValue
I see the default / main help and not subcmd help.
What can I do, to ensure that this last case shows me the subcmd help and not the main help? I thought the subparser structure would automatically procure the help from subcmd as found in the third case, but it is not so? Why?
The unrecognized args error is raised by parse_args
def parse_args(self, args=None, namespace=None):
args, argv = self.parse_known_args(args, namespace)
if argv:
msg = _('unrecognized arguments: %s')
self.error(msg % ' '.join(argv))
return args
The subparser is called via the cmd_par.__call__ with:
subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
for key, value in vars(subnamespace).items():
setattr(namespace, key, value)
if arg_strings:
vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
That is it is called with parse_known_args, and it's extras are returned to the main as UNRECOGNIZED. So it's the main than handles these, not the subparser.
In the $ python myscript.py subcmd -i ClearlyWrongValue case, the subparser raises a ArgumentError which is caught and converted into a self.error call.
Similarly, the newish exit_on_error parameter handles this kind of ArgumentError, but does not handle the urecognized error. There was some discussion of this in the bug/issues.
If you used parse_known_args, the extras would be ['--non-existent-argument', 'WhateverValue'], without distinguishing which parser initially classified them as such.
I'm using the Python (version 3.9.4) library argparse to parse a small number of option flags. For a number of reasons, I'd like to handle errors in my code rather than argparse.ArgumentParser. Namely, how an unrecognized argument is handled. For example, if I ran my-python-program --foobar, I'd like to be able to catch an exception and perform work there. This testcase disables almost all of the errors I've tried, except for an invalid argument:
import argparse
import sys
try:
parser = argparse.ArgumentParser(add_help=False, exit_on_error=False, usage=None)
parser.add_argument("--help", action="store_true", default=False)
parser.add_argument("--hello", default="Hello, world!")
args = parser.parse_args()
print(args.help, args.hello)
except Exception as err:
print("a problem occurred!", file=sys.stderr)
print(f"error: {err}", file=sys.stderr)
Instead, running my-python-program --foobar gives me:
usage: my-python-program [--help] [--hello HELLO]
my-python-program: error: unrecognized arguments: --foobar
If you look at the Python argparse source code, you can see that it calls self.error on an error. This function (at the bottom of the file), by default, prints the error message and quits. You can override this method in a subclass to raise an error instead.
import argparse
import sys
class MyArgumentParser(argparse.ArgumentParser):
"""An argument parser that raises an error, instead of quits"""
def error(self, message):
raise ValueError(message)
try:
parser = MyArgumentParser(add_help=False, exit_on_error=False, usage=None)
parser.add_argument("--help", action="store_true", default=False)
parser.add_argument("--hello", default="Hello, world!")
args = parser.parse_args()
print(args.help, args.hello)
except Exception as err:
print("a problem occurred!", file=sys.stderr)
print(f"error: {err}", file=sys.stderr)
Output:
$ python3 test.py --foobar
a problem occurred!
error: unrecognized arguments: --foobar
Going off of Greg Haskin's answer in this question, I tried to make a unittest to check that argparse is giving the appropriate error when I pass it some args that are not present in the choices. However, unittest generates a false positive using the try/except statement below.
In addition, when I make a test using just a with assertRaises statement, argparse forces the system exit and the program does not execute any more tests.
I would like to be able to have a test for this, but maybe it's redundant given that argparse exits upon error?
#!/usr/bin/env python3
import argparse
import unittest
class sweep_test_case(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown_TE(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
try:
self.assertRaises(argparse.ArgumentError, self.parser.parse_args(args))
except SystemExit:
print("should give a false positive pass")
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
with self.assertRaises(argparse.ArgumentError):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
Errors:
Usage: temp.py [-h] -c {yellow,blue}
temp.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
E
usage: temp.py [-h] -c {yellow,blue}
temp.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
should give a false positive pass
.
======================================================================
ERROR: test_required_unknown (__main__.sweep_test_case)
Try to perform sweep on something that isn't an option.
----------------------------------------------------------------------
Traceback (most recent call last): #(I deleted some lines)
File "/Users/darrin/anaconda/lib/python3.5/argparse.py", line 2310, in _check_value
raise ArgumentError(action, msg % args)
argparse.ArgumentError: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
During handling of the above exception, another exception occurred:
Traceback (most recent call last): #(I deleted some lines)
File "/anaconda/lib/python3.5/argparse.py", line 2372, in exit
_sys.exit(status)
SystemExit: 2
The trick here is to catch SystemExit instead of ArgumentError. Here's your test rewritten to catch SystemExit:
#!/usr/bin/env python3
import argparse
import unittest
class SweepTestCase(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown(self):
""" Try to perform sweep on something that isn't an option. """
args = ["--color", "NADA"]
with self.assertRaises(SystemExit):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
That now runs correctly, and the test passes:
$ python scratch.py
usage: scratch.py [-h] -c {yellow,blue}
scratch.py: error: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
However, you can see that the usage message is getting printed, so your test output is a bit messed up. It might also be nice to check that the usage message contains "invalid choice".
You can do that by patching sys.stderr:
#!/usr/bin/env python3
import argparse
import unittest
from io import StringIO
from unittest.mock import patch
class SweepTestCase(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
#patch('sys.stderr', new_callable=StringIO)
def test_required_unknown(self, mock_stderr):
""" Try to perform sweep on something that isn't an option. """
args = ["--color", "NADA"]
with self.assertRaises(SystemExit):
self.parser.parse_args(args)
self.assertRegexpMatches(mock_stderr.getvalue(), r"invalid choice")
if __name__ == '__main__':
unittest.main()
Now you only see the regular test report:
$ python scratch.py
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
For pytest users, here's the equivalent that doesn't check the message.
import argparse
import pytest
def test_required_unknown():
""" Try to perform sweep on something that isn't an option. """
parser=argparse.ArgumentParser()
parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
args = ["--color", "NADA"]
with pytest.raises(SystemExit):
parser.parse_args(args)
Pytest captures stdout/stderr by default, so it doesn't pollute the test report.
$ pytest scratch.py
================================== test session starts ===================================
platform linux -- Python 3.6.7, pytest-3.5.0, py-1.7.0, pluggy-0.6.0
rootdir: /home/don/.PyCharm2018.3/config/scratches, inifile:
collected 1 item
scratch.py . [100%]
================================ 1 passed in 0.01 seconds ================================
You can also check the stdout/stderr contents with pytest:
import argparse
import pytest
def test_required_unknown(capsys):
""" Try to perform sweep on something that isn't an option. """
parser=argparse.ArgumentParser()
parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
args = ["--color", "NADA"]
with pytest.raises(SystemExit):
parser.parse_args(args)
stderr = capsys.readouterr().err
assert 'invalid choice' in stderr
As usual, I find pytest easier to use, but you can make it work in either one.
While the parser may raise an ArgumentError during parsing a specific argument, that is normally trapped, and passed to parser.error and parse.exit. The result is that the usage is printed, along with an error message, and then sys.exit(2).
So asssertRaises is not a good way of testing for this kind of error in argparse. The unittest file for the module, test/test_argparse.py has an elaborate way of getting around this, the involves subclassing the ArgumentParser, redefining its error method, and redirecting output.
parser.parse_known_args (which is called by parse_args) ends with:
try:
namespace, args = self._parse_known_args(args, namespace)
if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
return namespace, args
except ArgumentError:
err = _sys.exc_info()[1]
self.error(str(err))
=================
How about this test (I've borrowed several ideas from test_argparse.py:
import argparse
import unittest
class ErrorRaisingArgumentParser(argparse.ArgumentParser):
def error(self, message):
#print(message)
raise ValueError(message) # reraise an error
class sweep_test_case(unittest.TestCase):
"""Tests that the Parse class works correctly"""
def setUp(self):
self.parser=ErrorRaisingArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should pass"""
args = ["--color", "NADA"]
with self.assertRaises(ValueError) as cm:
self.parser.parse_args(args)
print('msg:',cm.exception)
self.assertIn('invalid choice', str(cm.exception))
if __name__ == '__main__':
unittest.main()
with a run:
1931:~/mypy$ python3 stack39028204.py
msg: argument -c/--color: invalid choice: 'NADA' (choose from 'yellow', 'blue')
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
With many of the great answers above, I see that in the setUp method a parser instance is created inside our test and an argument is added to it, effectively causing the test to be of argparse's implementation. This, of course, could be a valid test/use case but wouldn't necessarily test a script's or application's specific use of argparse.
I think Yauhen Yakimovich's answer gives good insight into how to make use of argparse in a pragmatic way. While I haven't embraced it fully, I thought a simplified test method is possible via a parser generator and an override.
I've opted for testing my code rather than argparse's implementation. To achieve this we'll want to utilize a factory to create the parser in our code that holds all the argument definitions. This facilitates testing our own parser in setUp.
// my_class.py
import argparse
class MyClass:
def __init__(self):
self.parser = self._create_args_parser()
def _create_args_parser():
parser = argparse.ArgumentParser()
parser.add_argument('--kind',
action='store',
dest='kind',
choices=['type1', 'type2'],
help='kind can be any of: type1, type2')
return parser
In our test, we can generate our parser and test against it. We will override the error method to ensure we don't get trapped in argparse's ArgumentError evaluation.
import unittest
from my_class import MyClass
class MyClassTest(unittest.TestCase):
def _redefine_parser_error_method(self, message):
raise ValueError
def setUp(self):
parser = MyClass._create_args_parser()
parser.error = self._redefine_parser_error_func
self.parser = parser
def test_override_certificate_kind_arguments(self):
args = ['--kind', 'not-supported']
expected_message = "argument --kind: invalid choice: 'not-supported'.*$"
with self.assertRaisesRegex(ValueError, expected_message):
self.parser.parse_args(args)
This might not be the absolute best answer but I find it nice to use our own parser's arguments and test that part by simply testing against an exception we know should only happen in the test itself.
If you look in the error log, you can see that a argparse.ArgumentError is raised and not an AttributeError. your code should look like this:
#!/usr/bin/env python3
import argparse
import unittest
from argparse import ArgumentError
class sweep_test_case(unittest.TestCase):
"""Tests that the merParse class works correctly"""
def setUp(self):
self.parser=argparse.ArgumentParser()
self.parser.add_argument(
"-c", "--color",
type=str,
choices=["yellow", "blue"],
required=True)
def test_required_unknown_TE(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
try:
self.assertRaises(ArgumentError, self.parser.parse_args(args))
except SystemExit:
print("should give a false positive pass")
def test_required_unknown(self):
"""Try to perform sweep on something that isn't an option.
Should return an attribute error if it fails.
This test incorrectly shows that the test passed, even though that must
not be true."""
args = ["--color", "NADA"]
with self.assertRaises(ArgumentError):
self.parser.parse_args(args)
if __name__ == '__main__':
unittest.main()
If you look into the source code of argparse, in argparse.py, around line 1732 (my python version is 3.5.1), there is a method of ArgumentParser called parse_known_args. The code is:
# parse the arguments and exit if there are any errors
try:
namespace, args = self._parse_known_args(args, namespace)
if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
return namespace, args
except ArgumentError:
err = _sys.exc_info()[1]
self.error(str(err))
So, the ArgumentError will be swallowed by argparse, and exit with an error code. If you want to test this anyway, the only way I could think out is mocking sys.exc_info.
I know this is an old question but just to expand on #don-kirkby's answer of looking for SystemExit – but without having to use pytest or patching – you can wrap the testcode in contextlib.redirect_stderr, if you want to assert something about the error message:
import contextlib
from io import StringIO
import unittest
class MyTest(unittest.TestCase):
def test_foo(self):
ioerr = StringIO()
with contextlib.redirect_stderr(ioerr):
with self.assertRaises(SystemExit) as err:
foo('bad')
self.assertEqual(err.exception.code, 2)
self.assertIn("That is a 'bad' thing", ioerr.getvalue())
I had a similar problem with the same error of argparse (exit 2) and corrected it capturing the first element of tuple that parse_known_args() return, an argparse.Namespace object.
def test_basics_options_of_parser(self):
parser = w2ptdd.get_parser()
# unpacking tuple
parser_name_space,__ = parser.parse_known_args()
args = vars(parser_name_space)
self.assertFalse(args['unit'])
self.assertFalse(args['functional'])
I'm writing a script which I want to distribute using Setuptools. I have added this script to the entry_points section in my setup.py.
From the setuptools docs:
The functions you specify are called with no arguments, and their return value is passed to sys.exit(), so you can return an errorlevel or message to print to stderr.
Since the method will return instead of exit it becomes more testable. For testability purposes I accept arguments in the method defaulting to sys.argv. So far so good.
The problem arises when argparse is added to the mix. When argparse fails to parse args it calls sys.exit. Now I would really prefer that argparse doesn't do this as this is handled by the setuptools wrapper. The first thing I could think of to fix this is to override the argparse.ArgumentParser but then I saw this:
# ===============
# Exiting methods
# ===============
def exit(self, status=0, message=None):
if message:
self._print_message(message, _sys.stderr)
_sys.exit(status)
def error(self, message):
"""error(message: string)
Prints a usage message incorporating the message to stderr and
exits.
If you override this in a subclass, it should not return -- it
should either exit or raise an exception.
"""
self.print_usage(_sys.stderr)
self.exit(2, _('%s: error: %s\n') % (self.prog, message))
So the docstring states I should not return and stick with raising an exception. How should I solve this?
The main method if I didn't explain it thoroughly enough:
def main(args=sys.argv):
parser = ArgumentParser(prog='spam')
# parser is configured here
parsed = parser.parse_args(args)
# Parsed args are used here
The reason you don't want to return from error is that the parser will continue parsing. Some errors are raised near the end (e.g. about unparsed strings), but others can occur early (e.g. bad type for the first argument string). The behavior of parse_args is unpredictable if you return from the error method. Normally you want the parser to quit and return control your code.
What you want to do is wrap the parse_args() call in a try: except SystemExit: block. I often use test scripts like this:
for test in ['-o FILE',
...
]:
print(test)
try:
print(parser.parse_args(test.split()))
except SystemExit:
pass
You could use error and/or exit to return other kinds of Exceptions. They could also bypass the usage message. But in one way or other you need to trap the exception in your wrapper.
If you're starting on a fresh project or have time for some refactoring, then you might consider using the Click library. Click has both setuptools integration and 'testability' as features, among other considerations.
Here's an example / test-snippet from the docs that both creates a mini command-line interface, and then tests it immediately:
import click
from click.testing import CliRunner
def test_hello_world():
#click.command()
#click.argument('name')
def hello(name):
click.echo('Hello %s!' % name)
runner = CliRunner()
result = runner.invoke(hello, ['Peter'])
assert result.exit_code == 0
assert result.output == 'Hello Peter!\n'
Apart from tinkering with the argparse source, is there any way to control the exit status code should there be a problem when parse_args() is called, for example, a missing required switch?
I'm not aware of any mechanism to specify an exit code on a per-argument basis. You can catch the SystemExit exception raised on .parse_args() but I'm not sure how you would then ascertain what specifically caused the error.
EDIT: For anyone coming to this looking for a practical solution, the following is the situation:
ArgumentError() is raised appropriately when arg parsing fails. It is passed the argument instance and a message
ArgumentError() does not store the argument as an instance attribute, despite being passed (which would be convenient)
It is possible to re-raise the ArgumentError exception by subclassing ArgumentParser, overriding .error() and getting hold of the exception from sys.exc_info()
All that means the following code - whilst ugly - allows us to catch the ArgumentError exception, get hold of the offending argument and error message, and do as we see fit:
import argparse
import sys
class ArgumentParser(argparse.ArgumentParser):
def _get_action_from_name(self, name):
"""Given a name, get the Action instance registered with this parser.
If only it were made available in the ArgumentError object. It is
passed as it's first arg...
"""
container = self._actions
if name is None:
return None
for action in container:
if '/'.join(action.option_strings) == name:
return action
elif action.metavar == name:
return action
elif action.dest == name:
return action
def error(self, message):
exc = sys.exc_info()[1]
if exc:
exc.argument = self._get_action_from_name(exc.argument_name)
raise exc
super(ArgumentParser, self).error(message)
## usage:
parser = ArgumentParser()
parser.add_argument('--foo', type=int)
try:
parser.parse_args(['--foo=d'])
except argparse.ArgumentError, exc:
print exc.message, '\n', exc.argument
Not tested in any useful way. The usual don't-blame-me-if-it-breaks indemnity applies.
All the answers nicely explain the details of argparse implementation.
Indeed, as proposed in PEP (and pointed by Rob Cowie) one should inherit ArgumentParser and override the behavior of error or exit methods.
In my case I just wanted to replace usage print with full help print in case of the error:
class ArgumentParser(argparse.ArgumentParser):
def error(self, message):
self.print_help(sys.stderr)
self.exit(2, '%s: error: %s\n' % (self.prog, message))
In case of override main code will continue to contain the minimalistic..
# Parse arguments.
args = parser.parse_args()
# On error this will print help and cause exit with explanation message.
Perhaps catching the SystemExit exception would be a simple workaround:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('foo')
try:
args = parser.parse_args()
except SystemExit:
print("do something else")
Works for me, even in an interactive session.
Edit: Looks like #Rob Cowie beat me to the switch. Like he said, this doesn't have very much diagnostic potential, unless you want get silly and try to glean info from the traceback.
As of Python 3.9, this is no longer so painful. You can now handle this via the new argparse.ArgumentParser exit_on_error instantiation argument. Here is an example (slightly modified from the python docs: argparse#exit_on_error):
parser = argparse.ArgumentParser(exit_on_error=False)
parser.add_argument('--integers', type=int)
try:
parser.parse_args('--integers a'.split())
except argparse.ArgumentError:
print('Catching an argumentError')
exit(-1)
You'd have to tinker. Look at argparse.ArgumentParser.error, which is what gets called internally. Or you could make the arguments non-mandatory, then check and exit outside argparse.
You can use one of the exiting methods: http://docs.python.org/library/argparse.html#exiting-methods. It should already handle situations where the arguments are invalid, however (assuming you have defined your arguments properly).
Using invalid arguments:
% [ $(./test_argparse.py> /dev/null 2>&1) ] || { echo error }
error # exited with status code 2
I needed a simple method to catch an argparse error at application start and pass the error to a wxPython form. Combining the best answers from above resulted in the following small solution:
import argparse
# sub class ArgumentParser to catch an error message and prevent application closing
class MyArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
super(MyArgumentParser, self).__init__(*args, **kwargs)
self.error_message = ''
def error(self, message):
self.error_message = message
def parse_args(self, *args, **kwargs):
# catch SystemExit exception to prevent closing the application
result = None
try:
result = super().parse_args(*args, **kwargs)
except SystemExit:
pass
return result
# testing -------
my_parser = MyArgumentParser()
my_parser.add_argument('arg1')
my_parser.parse_args()
# check for an error
if my_parser.error_message:
print(my_parser.error_message)
running it:
>python test.py
the following arguments are required: arg1
While argparse.error is a method and not a class its not possible to "try", "except" all "unrecognized arguments" errors. If you want to do so you need to override the error function from argparse:
def print_help(errmsg):
print(errmsg.split(' ')[0])
parser.error = print_help
args = parser.parse_args()
on an invalid input it will now print:
unrecognised